diff options
author | Einar Egilsson <einar@einaregilsson.com> | 2015-09-18 15:42:21 +0000 |
---|---|---|
committer | Einar Egilsson <einar@einaregilsson.com> | 2015-09-18 15:42:21 +0000 |
commit | b2ba3216ea5dbe6a74fdfa75611fce95c1481316 (patch) | |
tree | 0ed80d05edc5497d64455197a04fabec7aa8d69d | |
parent | 862d738c79dc6f8188d3f9fefd46c01179041710 (diff) |
Halfway to making Firefox compatible
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | background.js | 222 | ||||
-rwxr-xr-x | build.py | 2 | ||||
-rw-r--r-- | js/background.js | 18 | ||||
-rw-r--r-- | js/firefox/background-shim.js | 108 | ||||
-rw-r--r-- | js/firefox/content-script-proxy.js | 16 | ||||
-rw-r--r-- | js/firefox/extension-storage.jsm | 152 | ||||
-rw-r--r-- | js/firefox/page-shim.js | 62 | ||||
-rw-r--r-- | js/popup.js | 2 | ||||
-rw-r--r-- | js/redirect.js | 5 | ||||
-rw-r--r-- | manifest.json | 1 | ||||
-rw-r--r-- | package.json | 14 | ||||
-rw-r--r-- | popup.html | 1 | ||||
-rw-r--r-- | redirector.html | 3 | ||||
-rw-r--r-- | test/test-index.js | 19 |
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 @@ -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" +} @@ -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); |