aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--background.js222
-rwxr-xr-xbuild.py2
-rw-r--r--js/background.js18
-rw-r--r--js/firefox/background-shim.js108
-rw-r--r--js/firefox/content-script-proxy.js16
-rw-r--r--js/firefox/extension-storage.jsm152
-rw-r--r--js/firefox/page-shim.js62
-rw-r--r--js/popup.js2
-rw-r--r--js/redirect.js5
-rw-r--r--manifest.json1
-rw-r--r--package.json14
-rw-r--r--popup.html1
-rw-r--r--redirector.html3
-rw-r--r--test/test-index.js19
15 files changed, 618 insertions, 9 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e382ce8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+#Redirector
+A basic add-on \ No newline at end of file
diff --git a/background.js b/background.js
new file mode 100644
index 0000000..4dbbdd3
--- /dev/null
+++ b/background.js
@@ -0,0 +1,222 @@
+
+//This is the background script. It is responsible for actually redirecting requests,
+//as well as monitoring changes in the redirects and the disabled status and reacting to them.
+
+//TODO: Better browser detection...
+var isFirefox = false;
+
+if (typeof chrome == 'undefined') {
+ isFirefox = true;
+ var firefoxShim = require('./firefox/shim');
+ chrome = firefoxShim.chrome;
+ Redirect = firefoxShim.Redirect;
+}
+//Hopefully Firefox will fix this at some point and we can just use onBeforeRequest everywhere...
+var redirectEvent = isFirefox ? chrome.webRequest.onBeforeSendHeaders : chrome.webRequest.onBeforeRequest;
+
+//Redirects partitioned by request type, so we have to run through
+//the minimum number of redirects for each request.
+var partitionedRedirects = {};
+
+//Keep track of tabids where the main_frame url has been redirected.
+//Mark it as green until a new url is loaded.
+var tabIdToIcon = {
+
+};
+
+//Cache of urls that have just been redirected to. They will not be redirected again, to
+//stop recursive redirects, and endless redirect chains.
+//Key is url, value is timestamp of redirect.
+var ignoreNextRequest = {
+
+};
+
+function log(msg) {
+ if (log.enabled) {
+ console.log('REDIRECTOR: ' + msg);
+ }
+}
+log.enabled = true;
+
+function setIcon(image19, image38, tabId) {
+ var data = {
+ path: {
+ 19: image19,
+ 38: image38
+ }
+ };
+ if (typeof tabId !== 'undefined') {
+ data.tabId = tabId;
+ }
+ chrome.browserAction.setIcon(data, function(tab) {
+ var err = chrome.runtime.lastError;
+ if (err) {
+ //If not checked we will get unchecked errors in the background page console...
+ log('Error in SetIcon: ' + err.message);
+ }
+ });
+}
+
+//This is the actual function that gets called for each request and must
+//decide whether or not we want to redirect.
+function checkRedirects(details) {
+
+ //We only allow GET request to be redirected, don't want to accidentally redirect
+ //sensitive POST parameters
+ if (details.method != 'GET') {
+ return null;
+ }
+
+ log('Checking: ' + details.type + ': ' + details.url);
+
+ var list = partitionedRedirects[details.type];
+ if (!list) {
+ log('No list for type: ' + details.type);
+ return null;
+ }
+
+ var timestamp = ignoreNextRequest[details.url];
+ if (timestamp) {
+ log('Ignoring ' + details.url + ', was just redirected ' + (new Date().getTime()-timestamp) + 'ms ago');
+ delete ignoreNextRequest[details.url];
+ return null;
+ }
+
+ for (var i = 0; i < list.length; i++) {
+ var r = list[i];
+ var result = r.getMatch(details.url);
+
+ if (result.isMatch) {
+
+ log('Redirecting ' + details.url + ' ===> ' + result.redirectTo + ', type: ' + details.type + ', pattern: ' + r.includePattern);
+
+ /* Unfortunately the setBrowserIcon for a specific tab function is way too unreliable, fails all the time with tab not found,
+ even though the tab is there. So, for now I'm cancelling this feature, which would have been pretty great ... :/
+ if (details.type == 'main_frame') {
+ log('Setting icon on tab ' + details.tabId + ' to green');
+
+ setIcon("images/icon19redirected.png", "images/icon38redirected.png", details.tabId);
+ tabIdToIcon[details.tabId] = true;
+ }*/
+ ignoreNextRequest[result.redirectTo] = new Date().getTime();
+
+ return { redirectUrl: result.redirectTo };
+ }
+ }
+
+ /* Cancelled for now because of setBrowserIcon being really unreliable...
+ if (details.type == 'main_frame' && tabIdToIcon[details.tabId]) {
+ log('Setting icon on tab ' + details.tabId + ' back to active');
+ setIcon("images/icon19active.png", "images/icon38active.png", details.tabId);
+ delete tabIdToIcon[details.tabId];
+ }*/
+
+ return null;
+}
+
+//Monitor changes in data, and setup everything again.
+//This could probably be optimized to not do everything on every change
+//but why bother?
+chrome.storage.onChanged.addListener(function(changes, namespace) {
+ if (changes.disabled) {
+ updateIcon();
+
+ if (changes.disabled.newValue == true) {
+ log('Disabling Redirector, removing listener');
+ redirectEvent.removeListener(checkRedirects);
+ } else {
+ log('Enabling Redirector, setting up listener');
+ setUpRedirectListener();
+ }
+ }
+
+ if (changes.redirects) {
+ log('Redirects have changed, setting up listener again');
+ setUpRedirectListener();
+ }
+});
+
+//Creates a filter to pass to the listener so we don't have to run through
+//all the redirects for all the request types we don't have any redirects for anyway.
+function createFilter(redirects) {
+ var types = [];
+ for (var i = 0; i < redirects.length; i++) {
+ redirects[i].appliesTo.forEach(function(type) {
+ if (types.indexOf(type) == -1) {
+ types.push(type);
+ }
+ });
+ }
+ types.sort();
+
+ return {
+ urls: ["http://*/*", "https://*/*"],
+ types : types
+ };
+}
+
+function createPartitionedRedirects(redirects) {
+ var partitioned = {};
+
+ for (var i = 0; i < redirects.length; i++) {
+ var redirect = new Redirect(redirects[i]);
+ redirect.compile();
+ for (var j=0; j<redirect.appliesTo.length;j++) {
+ var requestType = redirect.appliesTo[j];
+ if (partitioned[requestType]) {
+ partitioned[requestType].push(redirect);
+ } else {
+ partitioned[requestType] = [redirect];
+ }
+ }
+ }
+ return partitioned;
+}
+
+//Sets up the listener, partitions the redirects, creates the appropriate filters etc.
+function setUpRedirectListener() {
+
+ redirectEvent.removeListener(checkRedirects); //Unsubscribe first, in case there are changes...
+
+ chrome.storage.local.get({redirects:'firstrun'}, function(obj) {
+ var redirects = obj.redirects;
+
+ if (redirects === 'firstrun') {
+ log('No redirects to set up, first run of extension');
+ //TODO: import old Firefox redirects
+ return;
+ }
+
+ if (redirects.length == 0) {
+ return;
+ }
+
+ partitionedRedirects = createPartitionedRedirects(redirects);
+ var filter = createFilter(redirects);
+
+ log('Setting filter for listener: ' + JSON.stringify(filter));
+ redirectEvent.addListener(checkRedirects, filter, ["blocking"]);
+ });
+}
+
+function updateIcon() {
+ chrome.storage.local.get({disabled:false}, function(obj) {
+ if (obj.disabled) {
+ setIcon("images/icon19disabled.png", "images/icon38disabled.png");
+ } else {
+ setIcon("images/icon19active.png", "images/icon38active.png");
+ }
+ });
+}
+
+//First time setup
+updateIcon();
+chrome.storage.local.get({disabled:false}, function(obj) {
+ if (!obj.disabled) {
+ setUpRedirectListener();
+ } else {
+ log('Redirector is disabled');
+ }
+});
+log('Redirector starting up...');
+ \ No newline at end of file
diff --git a/build.py b/build.py
index 741da58..37f39e2 100755
--- a/build.py
+++ b/build.py
@@ -36,7 +36,7 @@ def create_addon(files, browser):
del manifest['applications'] #Firefox specific, and causes warnings in other browsers...
if browser == 'opera':
- manifest['options_ui']['page'] = 'redirector.html' #Opera opens options in new tab, where the popup would look really ugly
+ manifest['options_ui']['page'] = 'data/redirector.html' #Opera opens options in new tab, where the popup would look really ugly
manifest['options_ui']['chrome_style'] = False
zf.writestr(f[2:], json.dumps(manifest, indent=2))
diff --git a/js/background.js b/js/background.js
index c86a4e2..4b2d1cf 100644
--- a/js/background.js
+++ b/js/background.js
@@ -3,9 +3,15 @@
//as well as monitoring changes in the redirects and the disabled status and reacting to them.
//TODO: Better browser detection...
-var isFirefox = !!navigator.userAgent.match(/Firefox\//);
-var storage = chrome.storage.local; //TODO: Change to sync when Firefox supports it...
-
+var isFirefox = false;
+
+if (!this.chrome) {
+ isFirefox = true;
+ var firefoxShim = require('./firefox/background-shim');
+ chrome = firefoxShim.chrome;
+ Redirect = firefoxShim.Redirect;
+ console.log(this.Redirect)
+}
//Hopefully Firefox will fix this at some point and we can just use onBeforeRequest everywhere...
var redirectEvent = isFirefox ? chrome.webRequest.onBeforeSendHeaders : chrome.webRequest.onBeforeRequest;
@@ -173,7 +179,7 @@ function setUpRedirectListener() {
redirectEvent.removeListener(checkRedirects); //Unsubscribe first, in case there are changes...
- storage.get({redirects:'firstrun'}, function(obj) {
+ chrome.storage.local.get({redirects:'firstrun'}, function(obj) {
var redirects = obj.redirects;
if (redirects === 'firstrun') {
@@ -195,7 +201,7 @@ function setUpRedirectListener() {
}
function updateIcon() {
- storage.get({disabled:false}, function(obj) {
+ chrome.storage.local.get({disabled:false}, function(obj) {
if (obj.disabled) {
setIcon("images/icon19disabled.png", "images/icon38disabled.png");
} else {
@@ -206,7 +212,7 @@ function updateIcon() {
//First time setup
updateIcon();
-storage.get({disabled:false}, function(obj) {
+chrome.storage.local.get({disabled:false}, function(obj) {
if (!obj.disabled) {
setUpRedirectListener();
} else {
diff --git a/js/firefox/background-shim.js b/js/firefox/background-shim.js
new file mode 100644
index 0000000..fb08039
--- /dev/null
+++ b/js/firefox/background-shim.js
@@ -0,0 +1,108 @@
+var self = require("sdk/self");
+
+const {Cu} = require('chrome');
+
+function makeUrl(relativeUrl) {
+ return self.data.url(relativeUrl).replace('/data/', '/');
+}
+//Get the extension storage from Nightly.
+Cu.import(makeUrl('js/firefox/extension-storage.jsm'));
+
+//Create the browser action:
+var { ToggleButton } = require("sdk/ui/button/toggle");
+var panels = require("sdk/panel");
+
+var button = ToggleButton({
+ id: "redirector",
+ label: "Redirector",
+ icon: {
+ "16": makeUrl("images/icon16active.png"),
+ "32": makeUrl("images/icon32active.png")
+ },
+ onChange: function(state) {
+ if (state.checked) {
+ panel.show({position: button});
+ }
+ }
+});
+
+var panel = panels.Panel({
+ width: 200,
+ height: 130,
+ contentURL: makeUrl('popup.html'),
+ contentScriptFile : makeUrl('js/firefox/content-script-proxy.js'),
+ onHide: function() {
+ button.state('window', {checked: false});
+ }
+});
+
+var extensionId = require('../../package.json').id;
+
+var chrome = {
+ webRequest : Cu.import('resource://gre/modules/WebRequest.jsm', {}),
+
+ storage : {
+ local : {
+ get : function(query, callback) {
+ ExtensionStorage.get(extensionId, query).then(callback);
+ },
+ set : function(data, callback) {
+ ExtensionStorage.set(extensionId, data).then(callback);
+ }
+ },
+
+ onChanged : {
+ addListener : function(listener) {
+ ExtensionStorage.addOnChangedListener(extensionId, listener);
+ },
+ removeListener : function(listener) {
+ ExtensionStorage.removeOnChangedListener(extensionId, listener);
+ }
+ }
+ },
+
+ runtime : {
+ },
+
+ browserAction : {
+ setIcon : function(data, callback) {
+
+ }
+ }
+};
+
+var pageMod = require("sdk/page-mod");
+
+function attachedPage(worker) {
+ worker.port.on('message', function(message) {
+ console.info('background got message: ' + JSON.stringify(message));
+
+ if (message.messageType == 'storage.get') {
+ console.info('Getting from storage');
+ chrome.storage.local.get(message.payload, function(data) {
+ var resultMsg = { messageId: message.messageId, payload: data };
+ console.info('background sending message: ' + JSON.stringify(resultMsg));
+ worker.port.emit('message', resultMsg);
+ });
+ } else if (message.messageType == 'storage.set') {
+ chrome.storage.local.set(message.payload, function(data) {
+ var resultMsg = { messageId: message.messageId, payload: data };
+ console.info('background sending message: ' + JSON.stringify(resultMsg));
+ worker.port.emit('message', resultMsg);
+ });
+ }
+ });
+}
+
+pageMod.PageMod({
+ include: makeUrl('redirector.html'),
+ contentScriptFile: makeUrl('js/firefox/content-script-proxy.js'),
+ onAttach : attachedPage
+});
+
+
+exports.chrome = chrome;
+
+//Get redirect.js, which is included in the background page in webextensions.
+exports.Redirect = require('../redirect').Redirect;
+
diff --git a/js/firefox/content-script-proxy.js b/js/firefox/content-script-proxy.js
new file mode 100644
index 0000000..58a6bc0
--- /dev/null
+++ b/js/firefox/content-script-proxy.js
@@ -0,0 +1,16 @@
+// This file listens to messages
+
+window.addEventListener('message', function(message) {
+ if (message.data.sender !== 'page') {
+ return;
+ }
+ console.info('proxy got page message: ' + JSON.stringify(message.data));
+
+ //Forward the message to the background script
+ self.port.emit('message', message.data);
+})
+
+self.port.on('message', function(message) {
+ console.info('proxy got chrome message: ' + JSON.stringify(message));
+ window.postMessage(message, '*');
+}); \ No newline at end of file
diff --git a/js/firefox/extension-storage.jsm b/js/firefox/extension-storage.jsm
new file mode 100644
index 0000000..02c59a3
--- /dev/null
+++ b/js/firefox/extension-storage.jsm
@@ -0,0 +1,152 @@
+//Copied from resource://gre/modules/ExtensionStorage.jsm in Nightly.
+//Will be removed when this addon moves to the WebExtensions API.
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ExtensionStorage"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/osfile.jsm")
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+
+let Path = OS.Path;
+let profileDir = OS.Constants.Path.profileDir;
+
+this.ExtensionStorage = {
+ cache: new Map(),
+ listeners: new Map(),
+
+ extensionDir: Path.join(profileDir, "browser-extension-data"),
+
+ getExtensionDir(extensionId) {
+ return Path.join(this.extensionDir, extensionId);
+ },
+
+ getStorageFile(extensionId) {
+ return Path.join(this.extensionDir, extensionId, "storage.js");
+ },
+
+ read(extensionId) {
+ if (this.cache.has(extensionId)) {
+ return this.cache.get(extensionId);
+ }
+
+ let path = this.getStorageFile(extensionId);
+ let decoder = new TextDecoder();
+ let promise = OS.File.read(path);
+ promise = promise.then(array => {
+ return JSON.parse(decoder.decode(array));
+ }).catch(() => {
+ Cu.reportError("Unable to parse JSON data for extension storage.");
+ return {};
+ });
+ this.cache.set(extensionId, promise);
+ return promise;
+ },
+
+ write(extensionId) {
+ let promise = this.read(extensionId).then(extData => {
+ let encoder = new TextEncoder();
+ let array = encoder.encode(JSON.stringify(extData));
+ let path = this.getStorageFile(extensionId);
+ OS.File.makeDir(this.getExtensionDir(extensionId), {ignoreExisting: true, from: profileDir});
+ let promise = OS.File.writeAtomic(path, array);
+ return promise;
+ }).catch(() => {
+ // Make sure this promise is never rejected.
+ Cu.reportError("Unable to write JSON data for extension storage.");
+ });
+
+ AsyncShutdown.profileBeforeChange.addBlocker(
+ "ExtensionStorage: Finish writing extension data",
+ promise);
+
+ return promise.then(() => {
+ AsyncShutdown.profileBeforeChange.removeBlocker(promise);
+ });
+ },
+
+ set(extensionId, items) {
+ return this.read(extensionId).then(extData => {
+ let changes = {};
+ for (let prop in items) {
+ changes[prop] = {oldValue: extData[prop], newValue: items[prop]};
+ extData[prop] = items[prop];
+ }
+
+ let listeners = this.listeners.get(extensionId);
+ if (listeners) {
+ for (let listener of listeners) {
+ listener(changes);
+ }
+ }
+
+ return this.write(extensionId);
+ });
+ },
+
+ remove(extensionId, items) {
+ return this.read(extensionId).then(extData => {
+ let changes = {};
+ for (let prop in items) {
+ changes[prop] = {oldValue: extData[prop]};
+ delete extData[prop];
+ }
+
+ let listeners = this.listeners.get(extensionId);
+ if (listeners) {
+ for (let listener of listeners) {
+ listener(changes);
+ }
+ }
+
+ return this.write(extensionId);
+ });
+ },
+
+ get(extensionId, keys) {
+ return this.read(extensionId).then(extData => {
+ let result = {};
+ if (keys === null) {
+ Object.assign(result, extData);
+ } else if (typeof(keys) == "object") {
+ for (let prop in keys) {
+ if (prop in extData) {
+ result[prop] = extData[prop];
+ } else {
+ result[prop] = keys[prop];
+ }
+ }
+ } else if (typeof(keys) == "string") {
+ result[prop] = extData[prop] || undefined;
+ } else {
+ for (let prop of keys) {
+ result[prop] = extData[prop] || undefined;
+ }
+ }
+
+ return result;
+ });
+ },
+
+ addOnChangedListener(extensionId, listener) {
+ let listeners = this.listeners.get(extensionId) || new Set();
+ listeners.add(listener);
+ this.listeners.set(extensionId, listeners);
+ },
+
+ removeOnChangedListener(extensionId, listener) {
+ let listeners = this.listeners.get(extensionId);
+ listeners.delete(listener);
+ },
+};
diff --git a/js/firefox/page-shim.js b/js/firefox/page-shim.js
new file mode 100644
index 0000000..fc63f89
--- /dev/null
+++ b/js/firefox/page-shim.js
@@ -0,0 +1,62 @@
+(function() {
+ //Communication functions for
+
+ var messageId = 1;
+ var callbacks = {};
+ function send(type, message, callback) {
+ var id = messageId++;
+ window.postMessage({sender:'page', messageId:id, messageType:type, payload:message}, '*');
+ callbacks[id] = callback;
+ }
+
+ window.addEventListener('message', function(message) {
+ if (message.data.sender == 'page') {
+ return; //Ignore messages we sent ourselves
+ }
+
+ console.info('page got message: ' + JSON.stringify(message.data));
+
+ var callback = callbacks[message.data.messageId];
+ if (callback) {
+ callback(message.data.payload);
+ delete callbacks[message.data.messageId];
+ }
+ });
+
+ window.chrome = {
+ storage : {
+ local : {
+ get : function(query, callback) {
+ send('storage.get', query, callback);
+ },
+ set : function(data, callback) {
+ send('storage.set', data, callback);
+ }
+ }
+ },
+
+ extension : {
+ getURL : function(file) {
+ return document.location.protocol + '//' + document.location.host + '/' + file;
+ }
+ },
+
+ tabs : {
+ query : function(data, callback) {
+
+ },
+
+ update : function(tabId, options, callback) {
+
+ }
+ },
+
+ runtime : {
+ getManifest : function() {
+ return { version : '3.0' };
+ }
+ }
+ };
+
+})();
+
diff --git a/js/popup.js b/js/popup.js
index 9aa13cf..1e9d353 100644
--- a/js/popup.js
+++ b/js/popup.js
@@ -20,7 +20,7 @@ angular.module('popupApp', []).controller('PopupCtrl', ['$scope', function($s) {
//switch to open one if we have it to minimize conflicts
var url = chrome.extension.getURL('redirector.html');
-
+
chrome.tabs.query({currentWindow:true, url:url}, function(tabs) {
if (tabs.length > 0) {
chrome.tabs.update(tabs[0].id, {active:true}, function(tab) {
diff --git a/js/redirect.js b/js/redirect.js
index f0b429d..c9036e1 100644
--- a/js/redirect.js
+++ b/js/redirect.js
@@ -3,6 +3,11 @@ function Redirect(o) {
this._init(o);
}
+//temp, allow addon sdk to require this.
+if (typeof exports !== 'undefined') {
+ exports.Redirect = Redirect;
+}
+
//Static
Redirect.WILDCARD = 'W';
Redirect.REGEX = 'R';
diff --git a/manifest.json b/manifest.json
index 5a1e28d..b0c7b8e 100644
--- a/manifest.json
+++ b/manifest.json
@@ -2,6 +2,7 @@
"manifest_version": 2,
"name": "Redirector",
+ "description": "Redirect pages based on user-defined patterns.",
"version": "3.0",
"icons": { "16": "images/icon16active.png",
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..38e4f23
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+ "title": "Redirector",
+ "name": "redirector",
+ "id" : "redirector@einaregilsson.com",
+ "version": "3.0.0",
+ "description": "A basic add-on",
+ "main": "js/background.js",
+ "author": "Einar Egilsson",
+ "engines": {
+ "firefox": ">=38.0a1",
+ "fennec": ">=38.0a1"
+ },
+ "license": "MIT"
+}
diff --git a/popup.html b/popup.html
index db72080..7ee5cb7 100644
--- a/popup.html
+++ b/popup.html
@@ -4,6 +4,7 @@
<title>REDIRECTOR</title>
<link rel="stylesheet" href="css/popup.css" />
<meta charset="UTF-8">
+ <script src="js/firefox/page-shim.js"></script>
<script src="js/angular.min.js"></script>
<script src="js/popup.js"></script>
</head>
diff --git a/redirector.html b/redirector.html
index 9ed22bf..1627d03 100644
--- a/redirector.html
+++ b/redirector.html
@@ -5,7 +5,8 @@
<meta charset="UTF-8">
<link rel="stylesheet" href="css/redirector.css" />
<!-- ☈ -->
- <link rel="shortcut icon" href="images/icon32active.png">
+ <link rel="shortcut icon" href="images/icon32active.png" />
+ <script src="js/firefox/page-shim.js"></script>
<script src="js/angular.min.js"></script>
<script src="js/redirect.js"></script>
<script src="js/app.js"></script>
diff --git a/test/test-index.js b/test/test-index.js
new file mode 100644
index 0000000..b3ad6e8
--- /dev/null
+++ b/test/test-index.js
@@ -0,0 +1,19 @@
+var main = require("../");
+
+exports["test main"] = function(assert) {
+ assert.pass("Unit test running!");
+};
+
+exports["test main async"] = function(assert, done) {
+ assert.pass("async Unit test running!");
+ done();
+};
+
+exports["test dummy"] = function(assert, done) {
+ main.dummy("foo", function(text) {
+ assert.ok((text === "foo"), "Is the text actually 'foo'");
+ done();
+ });
+};
+
+require("sdk/test").run(exports);