From b2ba3216ea5dbe6a74fdfa75611fce95c1481316 Mon Sep 17 00:00:00 2001 From: Einar Egilsson Date: Fri, 18 Sep 2015 15:42:21 +0000 Subject: Halfway to making Firefox compatible --- README.md | 2 + background.js | 222 +++++++++++++++++++++++++++++++++++++ build.py | 2 +- js/background.js | 18 ++- js/firefox/background-shim.js | 108 ++++++++++++++++++ js/firefox/content-script-proxy.js | 16 +++ js/firefox/extension-storage.jsm | 152 +++++++++++++++++++++++++ js/firefox/page-shim.js | 62 +++++++++++ js/popup.js | 2 +- js/redirect.js | 5 + manifest.json | 1 + package.json | 14 +++ popup.html | 1 + redirector.html | 3 +- test/test-index.js | 19 ++++ 15 files changed, 618 insertions(+), 9 deletions(-) create mode 100644 README.md create mode 100644 background.js create mode 100644 js/firefox/background-shim.js create mode 100644 js/firefox/content-script-proxy.js create mode 100644 js/firefox/extension-storage.jsm create mode 100644 js/firefox/page-shim.js create mode 100644 package.json create mode 100644 test/test-index.js 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 { + 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 @@ REDIRECTOR + diff --git a/redirector.html b/redirector.html index 9ed22bf..1627d03 100644 --- a/redirector.html +++ b/redirector.html @@ -5,7 +5,8 @@ - + + 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); -- cgit v1.2.3