| 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; |
| })(); |