Index: chrome/ext/background.js |
=================================================================== |
rename from chrome/background.js |
rename to chrome/ext/background.js |
--- a/chrome/background.js |
+++ b/chrome/ext/background.js |
@@ -1,6 +1,6 @@ |
/* |
* This file is part of Adblock Plus <http://adblockplus.org/>, |
- * Copyright (C) 2006-2013 Eyeo GmbH |
+ * Copyright (C) 2006-2014 Eyeo GmbH |
* |
* Adblock Plus is free software: you can redistribute it and/or modify |
* it under the terms of the GNU General Public License version 3 as |
@@ -17,184 +17,110 @@ |
(function() |
{ |
- /* Events */ |
+ /* Pages */ |
- var TabEventTarget = function() |
+ var Page = ext.Page = function(tab) |
{ |
- WrappedEventTarget.apply(this, arguments); |
+ this._id = tab.id; |
+ this._url = tab.url; |
- this._tabs = {}; |
+ this.browserAction = new BrowserAction(tab.id); |
+ this.contextMenus = new ContextMenus(this); |
+ }; |
+ Page.prototype = { |
+ get url() |
+ { |
+ // usually our Page objects are created from Chrome's Tab objects, which |
+ // provide the url. So we can return the url given in the constructor. |
+ if (this._url != null) |
+ return this._url; |
- this._sharedListener = this._sharedListener.bind(this); |
- this._removeTab = this._removeTab.bind(this); |
- }; |
- TabEventTarget.prototype = { |
- __proto__: WrappedEventTarget.prototype, |
- _bindToTab: function(tab) |
+ // but sometimes we only have the tab id when we create a Page object. |
+ // In that case we get the url from top frame of the tab, recorded by |
+ // the onBeforeRequest handler. |
+ var frames = framesOfTabs[this._id]; |
+ if (frames) |
+ { |
+ var frame = frames[0]; |
+ if (frame) |
+ return frame.url; |
+ } |
+ }, |
+ activate: function() |
{ |
- return { |
- addListener: function(listener) |
- { |
- var listeners = this._tabs[tab._id]; |
- |
- if (!listeners) |
- { |
- this._tabs[tab._id] = listeners = []; |
- |
- if (Object.keys(this._tabs).length == 1) |
- this.addListener(this._sharedListener); |
- |
- tab.onRemoved.addListener(this._removeTab); |
- } |
- |
- listeners.push(listener); |
- }.bind(this), |
- removeListener: function(listener) |
- { |
- var listeners = this._tabs[tab._id]; |
- if (!listeners) |
- return; |
- |
- var idx = listeners.indexOf(listener); |
- if (idx == -1) |
- return; |
- |
- listeners.splice(idx, 1); |
- |
- if (listeners.length == 0) |
- tab.onRemoved.removeListener(this._removeTab); |
- else |
- { |
- if (listeners.length > 1) |
- return; |
- if (listeners[0] != this._removeTab) |
- return; |
- } |
- |
- this._removeTab(tab); |
- }.bind(this) |
- }; |
+ chrome.tabs.update(this._id, {selected: true}); |
}, |
- _sharedListener: function(tab) |
+ sendMessage: function(message, responseCallback) |
{ |
- var listeners = this._tabs[tab._id]; |
- |
- if (!listeners) |
- return; |
- |
- // copy listeners before calling them, because they might |
- // add or remove other listeners, which must not be taken |
- // into account before the next occurrence of the event |
- listeners = listeners.slice(0); |
- |
- for (var i = 0; i < listeners.length; i++) |
- listeners[i](tab); |
- }, |
- _removeTab: function(tab) |
- { |
- delete this._tabs[tab._id]; |
- |
- if (Object.keys(this._tabs).length == 0) |
- this.removeListener(this._sharedListener); |
+ chrome.tabs.sendMessage(this._id, message, responseCallback); |
} |
}; |
- var LoadingTabEventTarget = function() |
- { |
- TabEventTarget.call(this, chrome.tabs.onUpdated); |
- }; |
- LoadingTabEventTarget.prototype = { |
- __proto__: TabEventTarget.prototype, |
- _wrapListener: function(listener) |
+ ext.pages = { |
+ open: function(url, callback) |
{ |
- return function(id, info, tab) |
+ if (callback) |
{ |
- if (info.status == "loading") |
- listener(new Tab(tab)); |
- }; |
- } |
+ chrome.tabs.create({url: url}, function(openedTab) |
+ { |
+ var onUpdated = function(tabId, changeInfo, tab) |
+ { |
+ if (tabId == openedTab.id && changeInfo.status == "complete") |
+ { |
+ chrome.tabs.onUpdated.removeListener(onUpdated); |
+ callback(new Page(tab)); |
+ } |
+ }; |
+ chrome.tabs.onUpdated.addListener(onUpdated); |
+ }); |
+ } |
+ else |
+ chrome.tabs.create({url: url}); |
+ }, |
+ query: function(info, callback) |
+ { |
+ var rawInfo = {}; |
+ for (var property in info) |
+ { |
+ switch (property) |
+ { |
+ case "active": |
+ case "lastFocusedWindow": |
+ rawInfo[property] = info[property]; |
+ } |
+ } |
+ |
+ chrome.tabs.query(rawInfo, function(tabs) |
+ { |
+ callback(tabs.map(function(tab) |
+ { |
+ return new Page(tab); |
+ })); |
+ }); |
+ }, |
+ onLoading: new ext._EventTarget() |
}; |
- var CompletedTabEventTarget = function() |
+ chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) |
{ |
- TabEventTarget.call(this, chrome.tabs.onUpdated); |
- }; |
- CompletedTabEventTarget.prototype = { |
- __proto__: TabEventTarget.prototype, |
- _wrapListener: function(listener) |
- { |
- return function(id, info, tab) |
- { |
- if (info.status == "complete") |
- listener(new Tab(tab)); |
- }; |
- } |
- }; |
+ if (changeInfo.status == "loading") |
+ ext.pages.onLoading._dispatch(new Page(tab)); |
+ }); |
- var ActivatedTabEventTarget = function() |
+ chrome.webNavigation.onBeforeNavigate.addListener(function(details) |
{ |
- TabEventTarget.call(this, chrome.tabs.onActivated); |
- }; |
- ActivatedTabEventTarget.prototype = { |
- __proto__: TabEventTarget.prototype, |
- _wrapListener: function(listener) |
- { |
- return function(info) |
- { |
- chrome.tabs.get(info.tabId, function(tab) |
- { |
- listener(new Tab(tab)); |
- }); |
- }; |
- } |
- } |
+ if (details.frameId == 0) |
+ ext._removeFromAllPageMaps(details.tabId); |
+ }); |
- var RemovedTabEventTarget = function() |
+ chrome.tabs.onRemoved.addListener(function(tabId) |
{ |
- TabEventTarget.call(this, chrome.tabs.onRemoved); |
- }; |
- RemovedTabEventTarget.prototype = { |
- __proto__: TabEventTarget.prototype, |
- _wrapListener: function(listener) |
- { |
- return function(id) { listener(new Tab({id: id})); }; |
- } |
- }; |
+ ext._removeFromAllPageMaps(tabId); |
+ delete framesOfTabs[tabId]; |
+ }); |
- var BeforeRequestEventTarget = function() |
- { |
- WrappedEventTarget.call(this, chrome.webRequest.onBeforeRequest); |
- }; |
- BeforeRequestEventTarget.prototype = { |
- __proto__: WrappedEventTarget.prototype, |
- _wrapListener: function(listener) |
- { |
- return function(details) |
- { |
- var tab = null; |
- if (details.tabId != -1) |
- tab = new Tab({id: details.tabId}); |
- |
- return {cancel: listener( |
- details.url, |
- details.type, |
- tab, |
- details.frameId, |
- details.parentFrameId |
- ) === false}; |
- }; |
- }, |
- _prepareExtraArguments: function(urls) |
- { |
- return [urls ? {urls: urls} : {}, ["blocking"]]; |
- } |
- }; |
- |
- |
- /* Tabs */ |
- |
- var sendMessage = chrome.tabs.sendMessage || chrome.tabs.sendRequest; |
+ /* Browser actions */ |
var BrowserAction = function(tabId) |
{ |
@@ -203,11 +129,14 @@ |
BrowserAction.prototype = { |
setIcon: function(path) |
{ |
- chrome.browserAction.setIcon({tabId: this._tabId, path: path}); |
- }, |
- setTitle: function(title) |
- { |
- chrome.browserAction.setTitle({tabId: this._tabId, title: title}); |
+ var paths = {}; |
+ for (var i = 1; i <= 2; i++) |
+ { |
+ var size = i * 19; |
+ paths[size] = path.replace("$size", size); |
+ } |
+ |
+ chrome.browserAction.setIcon({tabId: this._tabId, path: paths}); |
}, |
setBadge: function(badge) |
{ |
@@ -238,153 +167,202 @@ |
} |
}; |
- Tab = function(tab) |
+ |
+ /* Context menus */ |
+ |
+ var contextMenuItems = new ext.PageMap(); |
+ var contextMenuUpdating = false; |
+ |
+ var updateContextMenu = function() |
{ |
- this._id = tab.id; |
+ if (contextMenuUpdating) |
+ return; |
- this.url = tab.url; |
- this.browserAction = new BrowserAction(tab.id); |
+ contextMenuUpdating = true; |
- this.onLoading = ext.tabs.onLoading._bindToTab(this); |
- this.onCompleted = ext.tabs.onCompleted._bindToTab(this); |
- this.onActivated = ext.tabs.onActivated._bindToTab(this); |
- this.onRemoved = ext.tabs.onRemoved._bindToTab(this); |
+ chrome.tabs.query({active: true, lastFocusedWindow: true}, function(tabs) |
+ { |
+ chrome.contextMenus.removeAll(function() |
+ { |
+ contextMenuUpdating = false; |
+ |
+ if (tabs.length == 0) |
+ return; |
+ |
+ var items = contextMenuItems.get({_id: tabs[0].id}); |
+ |
+ if (!items) |
+ return; |
+ |
+ items.forEach(function(item) |
+ { |
+ chrome.contextMenus.create({ |
+ title: item.title, |
+ contexts: item.contexts, |
+ onclick: function(info, tab) |
+ { |
+ item.onclick(info.srcUrl, new Page(tab)); |
+ } |
+ }); |
+ }); |
+ }); |
+ }); |
}; |
- Tab.prototype = { |
- close: function() |
+ |
+ var ContextMenus = function(page) |
+ { |
+ this._page = page; |
+ }; |
+ ContextMenus.prototype = { |
+ create: function(item) |
{ |
- chrome.tabs.remove(this._id); |
+ var items = contextMenuItems.get(this._page); |
+ if (!items) |
+ contextMenuItems.set(this._page, items = []); |
+ |
+ items.push(item); |
+ updateContextMenu(); |
}, |
- activate: function() |
+ removeAll: function() |
{ |
- chrome.tabs.update(this._id, {selected: true}); |
- }, |
- sendMessage: function(message, responseCallback) |
- { |
- sendMessage(this._id, message, responseCallback); |
+ contextMenuItems.delete(this._page); |
+ updateContextMenu(); |
} |
}; |
- TabMap = function() |
+ chrome.tabs.onActivated.addListener(updateContextMenu); |
+ |
+ chrome.windows.onFocusChanged.addListener(function(windowId) |
{ |
- this._map = {}; |
- this.delete = this.delete.bind(this); |
- }; |
- TabMap.prototype = { |
- get: function(tab) |
- { |
- return (this._map[tab._id] || {}).value; |
- }, |
- set: function(tab, value) |
- { |
- if (!(tab._id in this._map)) |
- tab.onRemoved.addListener(this.delete); |
+ if (windowId != chrome.windows.WINDOW_ID_NONE) |
+ updateContextMenu(); |
+ }); |
- this._map[tab._id] = {tab: tab, value: value}; |
- }, |
- has: function(tab) |
- { |
- return tab._id in this._map; |
- }, |
- clear: function() |
- { |
- for (var id in this._map) |
- this.delete(this._map[id].tab); |
- } |
- }; |
- TabMap.prototype["delete"] = function(tab) |
+ |
+ /* Web requests */ |
+ |
+ var framesOfTabs = {__proto__: null}; |
+ |
+ ext.getFrame = function(tabId, frameId) |
{ |
- delete this._map[tab._id]; |
- tab.onRemoved.removeListener(this.delete); |
- }; |
- |
- |
- /* Windows */ |
- |
- Window = function(win) |
- { |
- this._id = win.id; |
- this.visible = win.status != "minimized"; |
- }; |
- Window.prototype = { |
- getAllTabs: function(callback) |
- { |
- chrome.tabs.query({windowId: this._id}, function(tabs) |
- { |
- callback(tabs.map(function(tab) { return new Tab(tab); })); |
- }); |
- }, |
- getActiveTab: function(callback) |
- { |
- chrome.tabs.query({windowId: this._id, active: true}, function(tabs) |
- { |
- callback(new Tab(tabs[0])); |
- }); |
- }, |
- openTab: function(url, callback) |
- { |
- var props = {windowId: this._id, url: url}; |
- |
- if (!callback) |
- chrome.tabs.create(props); |
- else |
- chrome.tabs.create(props, function(tab) |
- { |
- callback(new Tab(tab)); |
- }); |
- } |
- }; |
- |
- |
- /* API */ |
- |
- ext.windows = { |
- getAll: function(callback) |
- { |
- chrome.windows.getAll(function(windows) |
- { |
- callback(windows.map(function(win) |
- { |
- return new Window(win); |
- })); |
- }); |
- }, |
- getLastFocused: function(callback) |
- { |
- chrome.windows.getLastFocused(function(win) |
- { |
- callback(new Window(win)); |
- }); |
- } |
- }; |
- |
- ext.tabs = { |
- onLoading: new LoadingTabEventTarget(), |
- onCompleted: new CompletedTabEventTarget(), |
- onActivated: new ActivatedTabEventTarget(), |
- onRemoved: new RemovedTabEventTarget() |
+ return (framesOfTabs[tabId] || {})[frameId]; |
}; |
ext.webRequest = { |
- onBeforeRequest: new BeforeRequestEventTarget(), |
+ onBeforeRequest: new ext._EventTarget(true), |
handlerBehaviorChanged: chrome.webRequest.handlerBehaviorChanged |
}; |
- ext.contextMenus = { |
- create: function(title, contexts, onclick) |
+ chrome.tabs.query({}, function(tabs) |
+ { |
+ tabs.forEach(function(tab) |
{ |
- chrome.contextMenus.create({ |
- title: title, |
- contexts: contexts, |
- onclick: function(info, tab) |
+ chrome.webNavigation.getAllFrames({tabId: tab.id}, function(details) |
+ { |
+ if (details && details.length > 0) |
{ |
- onclick(info.srcUrl, new Tab(tab)); |
+ var frames = framesOfTabs[tab.id] = {__proto__: null}; |
+ |
+ for (var i = 0; i < details.length; i++) |
+ frames[details[i].frameId] = {url: details[i].url, parent: null}; |
+ |
+ for (var i = 0; i < details.length; i++) |
+ { |
+ var parentFrameId = details[i].parentFrameId; |
+ |
+ if (parentFrameId != -1) |
+ frames[details[i].frameId].parent = frames[parentFrameId]; |
+ } |
} |
}); |
- }, |
- removeAll: function(callback) |
+ }); |
+ }); |
+ |
+ chrome.webRequest.onBeforeRequest.addListener(function(details) |
+ { |
+ try |
{ |
- chrome.contextMenus.removeAll(callback); |
+ // the high-level code isn't interested in requests that aren't related |
+ // to a tab and since those can only be handled in Chrome, we ignore |
+ // them here instead of in the browser independent high-level code. |
+ if (details.tabId == -1) |
+ return; |
+ |
+ var isMainFrame = details.type == "main_frame" || ( |
+ // assume that the first request belongs to the top frame. Chrome |
+ // may give the top frame the type "object" instead of "main_frame". |
+ // https://code.google.com/p/chromium/issues/detail?id=281711 |
+ details.frameId == 0 && !(details.tabId in framesOfTabs) |
+ ); |
+ |
+ var frames = null; |
+ if (!isMainFrame) |
+ frames = framesOfTabs[details.tabId]; |
+ if (!frames) |
+ frames = framesOfTabs[details.tabId] = {__proto__: null}; |
+ |
+ var frame = null; |
+ if (!isMainFrame) |
+ { |
+ // we are looking for the frame that contains the element that |
+ // is about to load, however if a frame is loading the surrounding |
+ // frame is indicated by parentFrameId instead of frameId |
+ var frameId; |
+ if (details.type == "sub_frame") |
+ frameId = details.parentFrameId; |
+ else |
+ frameId = details.frameId; |
+ |
+ frame = frames[frameId] || frames[Object.keys(frames)[0]]; |
+ |
+ if (frame && !ext.webRequest.onBeforeRequest._dispatch(details.url, details.type, new Page({id: details.tabId}), frame)) |
+ return {cancel: true}; |
+ } |
+ |
+ if (isMainFrame || details.type == "sub_frame") |
+ frames[details.frameId] = {url: details.url, parent: frame}; |
} |
- }; |
+ catch (e) |
+ { |
+ // recent versions of Chrome cancel the request when an error occurs in |
+ // the onBeforeRequest listener. However in our case it is preferred, to |
+ // let potentially some ads through, rather than blocking legit requests. |
+ console.error(e); |
+ } |
+ }, {urls: ["<all_urls>"]}, ["blocking"]); |
+ |
+ |
+ /* Message passing */ |
+ |
+ chrome.runtime.onMessage.addListener(function(message, rawSender, sendResponse) |
+ { |
+ var sender = { |
+ page: new Page(rawSender.tab), |
+ frame: { |
+ url: rawSender.url, |
+ get parent() |
+ { |
+ var frames = framesOfTabs[rawSender.tab.id]; |
+ |
+ if (!frames) |
+ return null; |
+ |
+ for (var frameId in frames) |
+ { |
+ if (frames[frameId].url == rawSender.url) |
+ return frames[frameId].parent; |
+ } |
+ |
+ return frames[0]; |
+ } |
+ } |
+ }; |
+ |
+ return ext.onMessage._dispatch(message, sender, sendResponse); |
+ }); |
+ |
+ |
+ /* Storage */ |
+ |
+ ext.storage = localStorage; |
})(); |