Index: safari/ext/background.js |
=================================================================== |
rename from safari/background.js |
rename to safari/ext/background.js |
--- a/safari/background.js |
+++ b/safari/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,526 +17,628 @@ |
(function() |
{ |
- /* Tabs */ |
+ /* Pages */ |
- var TabEventTarget = function() |
+ var pages = {__proto__: null}; |
+ var pageCounter = 0; |
+ |
+ var Page = function(id, tab, url) |
{ |
- WrappedEventTarget.apply(this, arguments); |
+ this._id = id; |
+ this._tab = tab; |
+ this._frames = [{url: url, parent: null}]; |
+ |
+ if (tab.page) |
+ this._messageProxy = new ext._MessageProxy(tab.page); |
+ else |
+ // while the new tab page is shown on Safari 7, the 'page' property |
+ // of the tab is undefined, and we can't send messages to that page |
+ this._messageProxy = { |
+ handleRequest: function() {}, |
+ handleResponse: function() {}, |
+ sendMessage: function() {} |
+ }; |
+ |
+ this.browserAction = new BrowserAction(this); |
+ this.contextMenus = new ContextMenus(this); |
}; |
- TabEventTarget.prototype = { |
- __proto__: WrappedEventTarget.prototype, |
- _wrapListener: function(listener) |
- { |
- return function(event) |
- { |
- if (event.target instanceof SafariBrowserTab) |
- listener(new Tab(event.target)); |
- }; |
- } |
- }; |
- |
- var LoadingTabEventTarget = function(target) |
- { |
- WrappedEventTarget.call(this, target, "message", false); |
- }; |
- LoadingTabEventTarget.prototype = { |
- __proto__: WrappedEventTarget.prototype, |
- _wrapListener: function(listener) |
- { |
- return function (event) |
- { |
- if (event.name == "loading" && event.message == event.target.url) |
- listener(new Tab(event.target)); |
- }; |
- } |
- }; |
- |
- Tab = function(tab) |
- { |
- this._tab = tab; |
- |
- this._eventTarget = tab; |
- this._messageDispatcher = tab.page; |
- |
- this.onLoading = new LoadingTabEventTarget(tab); |
- this.onCompleted = new TabEventTarget(tab, "navigate", false); |
- this.onActivated = new TabEventTarget(tab, "activate", false); |
- this.onRemoved = new TabEventTarget(tab, "close", false); |
- }; |
- Tab.prototype = { |
+ Page.prototype = { |
get url() |
{ |
- return this._tab.url; |
- }, |
- close: function() |
- { |
- this._tab.close(); |
+ return this._frames[0].url; |
}, |
activate: function() |
{ |
this._tab.activate(); |
}, |
- sendMessage: sendMessage, |
- browserAction: { |
- setIcon: function(path) |
+ sendMessage: function(message, responseCallback) |
+ { |
+ this._messageProxy.sendMessage(message, responseCallback, {pageId: this._id}); |
+ } |
+ }; |
+ |
+ var isPageActive = function(page) |
+ { |
+ var tab = page._tab; |
+ return tab == tab.browserWindow.activeTab && page == tab._visiblePage; |
+ }; |
+ |
+ var forgetPage = function(id) |
+ { |
+ ext._removeFromAllPageMaps(id); |
+ |
+ delete pages[id]._tab._pages[id]; |
+ delete pages[id]; |
+ }; |
+ |
+ var replacePage = function(page) |
+ { |
+ var tab = page._tab; |
+ tab._visiblePage = page; |
+ |
+ for (var id in tab._pages) |
+ { |
+ if (id != page._id) |
+ forgetPage(id); |
+ } |
+ |
+ if (isPageActive(page)) |
+ updateToolbarItemForPage(page, tab.browserWindow); |
+ }; |
+ |
+ ext.pages = { |
+ open: function(url, callback) |
+ { |
+ var tab = safari.application.activeBrowserWindow.openTab(); |
+ tab.url = url; |
+ |
+ if (callback) |
{ |
- safari.extension.toolbarItems[0].image = safari.extension.baseURI + path; |
- }, |
- setTitle: function(title) |
+ var onLoading = function(page) |
+ { |
+ if (page._tab == tab) |
+ { |
+ ext.pages.onLoading.removeListener(onLoading); |
+ callback(page); |
+ } |
+ }; |
+ ext.pages.onLoading.addListener(onLoading); |
+ } |
+ }, |
+ query: function(info, callback) |
+ { |
+ var matchedPages = []; |
+ |
+ for (var id in pages) |
{ |
- safari.extension.toolbarItems[0].toolTip = title; |
- }, |
- setBadge: function(badge) |
+ var page = pages[id]; |
+ var win = page._tab.browserWindow; |
+ |
+ if ("active" in info && info.active != isPageActive(page)) |
+ continue; |
+ if ("lastFocusedWindow" in info && info.lastFocusedWindow != (win == safari.application.activeBrowserWindow)) |
+ continue; |
+ |
+ matchedPages.push(page); |
+ }; |
+ |
+ callback(matchedPages); |
+ }, |
+ onLoading: new ext._EventTarget() |
+ }; |
+ |
+ safari.application.addEventListener("close", function(event) |
+ { |
+ // this event is dispatched on closing windows and tabs. However when a |
+ // window is closed, it is first dispatched on each tab in the window and |
+ // then on the window itself. But we are only interested in closed tabs. |
+ if (!(event.target instanceof SafariBrowserTab)) |
+ return; |
+ |
+ // when a tab is closed, forget the previous page associated with that |
+ // tab. Note that it wouldn't be sufficient do that when the old page |
+ // is unloading, because Safari dispatches window.onunload only when |
+ // reloading the page or following links, but not when closing the tab. |
+ for (var id in event.target._pages) |
+ forgetPage(id); |
+ }, true); |
+ |
+ |
+ /* Browser actions */ |
+ |
+ var toolbarItemProperties = {}; |
+ |
+ var getToolbarItemForWindow = function(win) |
+ { |
+ for (var i = 0; i < safari.extension.toolbarItems.length; i++) |
+ { |
+ var toolbarItem = safari.extension.toolbarItems[i]; |
+ |
+ if (toolbarItem.browserWindow == win) |
+ return toolbarItem; |
+ } |
+ |
+ return null; |
+ }; |
+ |
+ var updateToolbarItemForPage = function(page, win) { |
+ var toolbarItem = getToolbarItemForWindow(win); |
+ if (!toolbarItem) |
+ return; |
+ |
+ for (var name in toolbarItemProperties) |
+ { |
+ var property = toolbarItemProperties[name]; |
+ |
+ if (page && property.pages.has(page)) |
+ toolbarItem[name] = property.pages.get(page); |
+ else |
+ toolbarItem[name] = property.global; |
+ } |
+ }; |
+ |
+ var BrowserAction = function(page) |
+ { |
+ this._page = page; |
+ }; |
+ BrowserAction.prototype = { |
+ _set: function(name, value) |
+ { |
+ var toolbarItem = getToolbarItemForWindow(this._page._tab.browserWindow); |
+ if (!toolbarItem) |
+ return; |
+ |
+ var property = toolbarItemProperties[name]; |
+ if (!property) |
+ property = toolbarItemProperties[name] = { |
+ pages: new ext.PageMap(), |
+ global: toolbarItem[name] |
+ }; |
+ |
+ property.pages.set(this._page, value); |
+ |
+ if (isPageActive(this._page)) |
+ toolbarItem[name] = value; |
+ }, |
+ setIcon: function(path) |
+ { |
+ this._set("image", safari.extension.baseURI + path.replace("$size", "16")); |
+ }, |
+ setBadge: function(badge) |
+ { |
+ if (!badge) |
+ this._set("badge", 0); |
+ else if ("number" in badge) |
+ this._set("badge", badge.number); |
+ } |
+ }; |
+ |
+ safari.application.addEventListener("activate", function(event) |
+ { |
+ // this event is also dispatched on windows that got focused. But we |
+ // are only interested in tabs, which became active in their window. |
+ if (!(event.target instanceof SafariBrowserTab)) |
+ return; |
+ |
+ // update the toolbar item for the page visible in the tab that just |
+ // became active. If we can't find that page (e.g. when a page was |
+ // opened in a new tab, and our content script didn't run yet), the |
+ // toolbar item of the window, is reset to its intial configuration. |
+ updateToolbarItemForPage(event.target._visiblePage, event.target.browserWindow); |
+ }, true); |
+ |
+ |
+ /* Context menus */ |
+ |
+ var contextMenuItems = new ext.PageMap(); |
+ |
+ var ContextMenus = function(page) |
+ { |
+ this._page = page; |
+ }; |
+ ContextMenus.prototype = { |
+ create: function(item) |
+ { |
+ var items = contextMenuItems.get(this._page); |
+ if (!items) |
+ contextMenuItems.set(this._page, items = []); |
+ |
+ items.push(item); |
+ }, |
+ removeAll: function() |
+ { |
+ contextMenuItems.delete(this._page); |
+ } |
+ }; |
+ |
+ safari.application.addEventListener("contextmenu", function(event) |
+ { |
+ var pageId = event.userInfo.pageId; |
+ if (!pageId) |
+ return; |
+ |
+ var page = pages[event.userInfo.pageId]; |
+ var items = contextMenuItems.get(page); |
+ if (!items) |
+ return; |
+ |
+ var context = event.userInfo.tagName; |
+ if (context == "img") |
+ context = "image"; |
+ if (!event.userInfo.srcUrl) |
+ context = null; |
+ |
+ for (var i = 0; i < items.length; i++) |
+ { |
+ // Supported contexts are: all, audio, image, video |
+ var menuItem = items[i]; |
+ if (menuItem.contexts.indexOf("all") == -1 && menuItem.contexts.indexOf(context) == -1) |
+ continue; |
+ |
+ event.contextMenu.appendContextMenuItem(i, menuItem.title); |
+ } |
+ }); |
+ |
+ safari.application.addEventListener("command", function(event) |
+ { |
+ var page = pages[event.userInfo.pageId]; |
+ var items = contextMenuItems.get(page); |
+ |
+ items[event.command].onclick(event.userInfo.srcUrl, page); |
+ }); |
+ |
+ |
+ /* Web requests */ |
+ |
+ ext.webRequest = { |
+ onBeforeRequest: new ext._EventTarget(true), |
+ handlerBehaviorChanged: function() {} |
+ }; |
+ |
+ |
+ /* Background page */ |
+ |
+ ext.backgroundPage = { |
+ getWindow: function() |
+ { |
+ return window; |
+ } |
+ }; |
+ |
+ |
+ /* Background page proxy (for access from content scripts) */ |
+ |
+ var backgroundPageProxy = { |
+ cache: new ext.PageMap(), |
+ |
+ registerObject: function(obj, objects) |
+ { |
+ var objectId = objects.indexOf(obj); |
+ |
+ if (objectId == -1) |
+ objectId = objects.push(obj) - 1; |
+ |
+ return objectId; |
+ }, |
+ serializeSequence: function(sequence, objects, memo) |
+ { |
+ if (!memo) |
+ memo = {specs: [], arrays: []}; |
+ |
+ var items = []; |
+ for (var i = 0; i < sequence.length; i++) |
+ items.push(this.serialize(sequence[i], objects, memo)); |
+ |
+ return items; |
+ }, |
+ serialize: function(obj, objects, memo) |
+ { |
+ if (typeof obj == "object" && obj != null || typeof obj == "function") |
{ |
- if (!badge) |
- safari.extension.toolbarItems[0].badge = 0; |
- else if ("number" in badge) |
- safari.extension.toolbarItems[0].badge = badge.number; |
+ if (obj.constructor == Array) |
+ { |
+ if (!memo) |
+ memo = {specs: [], arrays: []}; |
+ |
+ var idx = memo.arrays.indexOf(obj); |
+ if (idx != -1) |
+ return memo.specs[idx]; |
+ |
+ var spec = {type: "array"}; |
+ memo.specs.push(spec); |
+ memo.arrays.push(obj); |
+ |
+ spec.items = this.serializeSequence(obj, objects, memo); |
+ return spec; |
+ } |
+ |
+ if (obj.constructor != Date && obj.constructor != RegExp) |
+ return {type: "object", objectId: this.registerObject(obj, objects)}; |
+ } |
+ |
+ return {type: "value", value: obj}; |
+ }, |
+ createCallback: function(callbackId, pageId, frameId) |
+ { |
+ var proxy = this; |
+ |
+ return function() |
+ { |
+ var page = pages[pageId]; |
+ if (!page) |
+ return; |
+ |
+ var objects = proxy.cache.get(page); |
+ if (!objects) |
+ return; |
+ |
+ page._tab.page.dispatchMessage("proxyCallback", |
+ { |
+ pageId: pageId, |
+ frameId: frameId, |
+ callbackId: callbackId, |
+ contextId: proxy.registerObject(this, objects), |
+ args: proxy.serializeSequence(arguments, objects) |
+ }); |
+ }; |
+ }, |
+ deserialize: function(spec, objects, pageId, memo) |
+ { |
+ switch (spec.type) |
+ { |
+ case "value": |
+ return spec.value; |
+ case "hosted": |
+ return objects[spec.objectId]; |
+ case "callback": |
+ return this.createCallback(spec.callbackId, pageId, spec.frameId); |
+ case "object": |
+ case "array": |
+ if (!memo) |
+ memo = {specs: [], objects: []}; |
+ |
+ var idx = memo.specs.indexOf(spec); |
+ if (idx != -1) |
+ return memo.objects[idx]; |
+ |
+ var obj; |
+ if (spec.type == "array") |
+ obj = []; |
+ else |
+ obj = {}; |
+ |
+ memo.specs.push(spec); |
+ memo.objects.push(obj); |
+ |
+ if (spec.type == "array") |
+ for (var i = 0; i < spec.items.length; i++) |
+ obj.push(this.deserialize(spec.items[i], objects, pageId, memo)); |
+ else |
+ for (var k in spec.properties) |
+ obj[k] = this.deserialize(spec.properties[k], objects, pageId, memo); |
+ |
+ return obj; |
+ } |
+ }, |
+ getObjectCache: function(page) |
+ { |
+ var objects = this.cache.get(page); |
+ if (!objects) |
+ { |
+ objects = [window]; |
+ this.cache.set(page, objects); |
+ } |
+ return objects; |
+ }, |
+ fail: function(error) |
+ { |
+ if (error instanceof Error) |
+ error = error.message; |
+ return {succeed: false, error: error}; |
+ }, |
+ handleMessage: function(message) |
+ { |
+ var objects = this.getObjectCache(pages[message.pageId]); |
+ |
+ switch (message.type) |
+ { |
+ case "getProperty": |
+ var obj = objects[message.objectId]; |
+ |
+ try |
+ { |
+ var value = obj[message.property]; |
+ } |
+ catch (e) |
+ { |
+ return this.fail(e); |
+ } |
+ |
+ return {succeed: true, result: this.serialize(value, objects)}; |
+ case "setProperty": |
+ var obj = objects[message.objectId]; |
+ var value = this.deserialize(message.value, objects, message.pageId); |
+ |
+ try |
+ { |
+ obj[message.property] = value; |
+ } |
+ catch (e) |
+ { |
+ return this.fail(e); |
+ } |
+ |
+ return {succeed: true}; |
+ case "callFunction": |
+ var func = objects[message.functionId]; |
+ var context = objects[message.contextId]; |
+ |
+ var args = []; |
+ for (var i = 0; i < message.args.length; i++) |
+ args.push(this.deserialize(message.args[i], objects, message.pageId)); |
+ |
+ try |
+ { |
+ var result = func.apply(context, args); |
+ } |
+ catch (e) |
+ { |
+ return this.fail(e); |
+ } |
+ |
+ return {succeed: true, result: this.serialize(result, objects)}; |
+ case "inspectObject": |
+ var obj = objects[message.objectId]; |
+ var objectInfo = {properties: {}, isFunction: typeof obj == "function"}; |
+ |
+ Object.getOwnPropertyNames(obj).forEach(function(prop) |
+ { |
+ objectInfo.properties[prop] = { |
+ enumerable: Object.prototype.propertyIsEnumerable.call(obj, prop) |
+ }; |
+ }); |
+ |
+ if (obj.__proto__) |
+ objectInfo.prototypeId = this.registerObject(obj.__proto__, objects); |
+ |
+ if (obj == Object.prototype) |
+ objectInfo.prototypeOf = "Object"; |
+ if (obj == Function.prototype) |
+ objectInfo.prototypeOf = "Function"; |
+ |
+ return objectInfo; |
} |
} |
}; |
- TabMap = function() |
+ |
+ /* Message processing */ |
+ |
+ safari.application.addEventListener("message", function(event) |
{ |
- this._tabs = []; |
- this._values = []; |
+ switch (event.name) |
+ { |
+ case "canLoad": |
+ switch (event.message.category) |
+ { |
+ case "loading": |
+ var tab = event.target; |
+ var message = event.message; |
- this._onClosed = this._onClosed.bind(this); |
- }; |
- TabMap.prototype = |
- { |
- get: function(tab) { |
- var idx; |
+ var pageId; |
+ var frameId; |
- if (!tab || (idx = this._tabs.indexOf(tab._tab)) == -1) |
- return null; |
+ if (message.isTopLevel) |
+ { |
+ pageId = ++pageCounter; |
+ frameId = 0; |
- return this._values[idx]; |
- }, |
- set: function(tab, value) |
- { |
- var idx = this._tabs.indexOf(tab._tab); |
+ if (!('_pages' in tab)) |
+ tab._pages = {__proto__: null}; |
- if (idx != -1) |
- this._values[idx] = value; |
- else |
- { |
- this._tabs.push(tab._tab); |
- this._values.push(value); |
+ var page = new Page(pageId, tab, message.url); |
+ pages[pageId] = tab._pages[pageId] = page; |
- tab._tab.addEventListener("close", this._onClosed, false); |
- } |
- }, |
- has: function(tab) |
- { |
- return this._tabs.indexOf(tab._tab) != -1; |
- }, |
- clear: function() |
- { |
- while (this._tabs.length > 0) |
- this._delete(this._tabs[0]); |
- }, |
- _delete: function(tab) |
- { |
- var idx = this._tabs.indexOf(tab); |
+ // when a new page is shown, forget the previous page associated |
+ // with its tab, and reset the toolbar item if necessary. |
+ // Note that it wouldn't be sufficient to do that when the old |
+ // page is unloading, because Safari dispatches window.onunload |
+ // only when reloading the page or following links, but not when |
+ // you enter a new URL in the address bar. |
+ if (!message.isPrerendered) |
+ replacePage(page); |
- if (idx != -1) |
- { |
- this._tabs.splice(idx, 1); |
- this._values.splice(idx, 1); |
+ ext.pages.onLoading._dispatch(page); |
+ } |
+ else |
+ { |
+ var page; |
+ var parentFrame; |
- tab.removeEventListener("close", this._onClosed, false); |
- } |
- }, |
- _onClosed: function(event) |
- { |
- this._delete(event.target); |
- } |
- }; |
- TabMap.prototype["delete"] = function(tab) |
- { |
- this._delete(tab._tab); |
- }; |
+ var lastPageId; |
+ var lastPage; |
+ var lastPageTopLevelFrame; |
+ // find the parent frame and its page for this sub frame, |
+ // by matching its referrer with the URL of frames previously |
+ // loaded in the same tab. If there is more than one match, |
+ // the most recent loaded page and frame is preferred. |
+ for (var curPageId in tab._pages) |
+ { |
+ var curPage = pages[curPageId]; |
- /* Windows */ |
+ for (var i = 0; i < curPage._frames.length; i++) |
+ { |
+ var curFrame = curPage._frames[i]; |
- Window = function(win) |
- { |
- this._win = win; |
- } |
- Window.prototype = { |
- get visible() |
- { |
- return this._win.visible; |
- }, |
- getAllTabs: function(callback) |
- { |
- callback(this._win.tabs.map(function(tab) { return new Tab(tab); })); |
- }, |
- getActiveTab: function(callback) |
- { |
- callback(new Tab(this._win.activeTab)); |
- }, |
- openTab: function(url, callback) |
- { |
- var tab = this._win.openTab(); |
- tab.url = url; |
+ if (curFrame.url == message.referrer) |
+ { |
+ pageId = curPageId; |
+ page = curPage; |
+ parentFrame = curFrame; |
+ } |
- if (callback) |
- callback(new Tab(tab)); |
- } |
- }; |
+ if (i == 0) |
+ { |
+ lastPageId = curPageId; |
+ lastPage = curPage; |
+ lastPageTopLevelFrame = curFrame; |
+ } |
+ } |
+ } |
- if (safari.extension.globalPage.contentWindow == window) |
- { |
- /* Background page proxy */ |
+ // if we can't find the parent frame and its page, fall back to |
+ // the page most recently loaded in the tab and its top level frame |
+ if (!page) |
+ { |
+ pageId = lastPageId; |
+ page = lastPage; |
+ parentFrame = lastPageTopLevelFrame; |
+ } |
- var proxy = { |
- tabs: [], |
- objects: [], |
- |
- registerObject: function(obj, objects) |
- { |
- var objectId = objects.indexOf(obj); |
- |
- if (objectId == -1) |
- objectId = objects.push(obj) - 1; |
- |
- return objectId; |
- }, |
- serializeSequence: function(sequence, objects, memo) |
- { |
- if (!memo) |
- memo = {specs: [], arrays: []}; |
- |
- var items = []; |
- for (var i = 0; i < sequence.length; i++) |
- items.push(this.serialize(sequence[i], objects, memo)); |
- |
- return items; |
- }, |
- serialize: function(obj, objects, memo) |
- { |
- if (typeof obj == "object" && obj != null || typeof obj == "function") |
- { |
- if (obj.constructor == Array) |
- { |
- if (!memo) |
- memo = {specs: [], arrays: []}; |
- |
- var idx = memo.arrays.indexOf(obj); |
- if (idx != -1) |
- return memo.specs[idx]; |
- |
- var spec = {type: "array"}; |
- memo.specs.push(spec); |
- memo.arrays.push(obj); |
- |
- spec.items = this.serializeSequence(obj, objects, memo); |
- return spec; |
- } |
- |
- if (obj.constructor != Date && obj.constructor != RegExp) |
- return {type: "object", objectId: this.registerObject(obj, objects)}; |
- } |
- |
- return {type: "value", value: obj}; |
- }, |
- createCallback: function(callbackId, tab) |
- { |
- var proxy = this; |
- |
- return function() |
- { |
- var idx = proxy.tabs.indexOf(tab); |
- |
- if (idx != -1) { |
- var objects = proxy.objects[idx]; |
- |
- tab.page.dispatchMessage("proxyCallback", |
- { |
- callbackId: callbackId, |
- contextId: proxy.registerObject(this, objects), |
- args: proxy.serializeSequence(arguments, objects) |
- }); |
- } |
- }; |
- }, |
- deserialize: function(spec, objects, tab, memo) |
- { |
- switch (spec.type) |
- { |
- case "value": |
- return spec.value; |
- case "hosted": |
- return objects[spec.objectId]; |
- case "callback": |
- return this.createCallback(spec.callbackId, tab); |
- case "object": |
- case "array": |
- if (!memo) |
- memo = {specs: [], objects: []}; |
- |
- var idx = memo.specs.indexOf(spec); |
- if (idx != -1) |
- return memo.objects[idx]; |
- |
- var obj; |
- if (spec.type == "array") |
- obj = []; |
- else |
- obj = {}; |
- |
- memo.specs.push(spec); |
- memo.objects.push(obj); |
- |
- if (spec.type == "array") |
- for (var i = 0; i < spec.items.length; i++) |
- obj.push(this.deserialize(spec.items[i], objects, tab, memo)); |
- else |
- for (var k in spec.properties) |
- obj[k] = this.deserialize(spec.properties[k], objects, tab, memo); |
- |
- return obj; |
- } |
- }, |
- createObjectCache: function(tab) |
- { |
- var objects = [window]; |
- |
- this.tabs.push(tab); |
- this.objects.push(objects); |
- |
- tab.addEventListener("close", function() |
- { |
- var idx = this.tabs.indexOf(tab); |
- |
- if (idx != -1) |
- { |
- this.tabs.splice(idx, 1); |
- this.objects.splice(idx, 1); |
- } |
- }.bind(this)); |
- |
- return objects; |
- }, |
- getObjectCache: function(tab) |
- { |
- var idx = this.tabs.indexOf(tab); |
- var objects; |
- |
- if (idx != -1) |
- objects = this.objects[idx]; |
- else |
- objects = this.objects[idx] = this.createObjectCache(tab); |
- |
- return objects; |
- }, |
- fail: function(error) |
- { |
- if (error instanceof Error) |
- error = error.message; |
- return {succeed: false, error: error}; |
- }, |
- _handleMessage: function(message, tab) |
- { |
- var objects = this.getObjectCache(tab); |
- |
- switch (message.type) |
- { |
- case "getProperty": |
- var obj = objects[message.objectId]; |
- |
- try |
- { |
- var value = obj[message.property]; |
- } |
- catch (e) |
- { |
- return this.fail(e); |
+ frameId = page._frames.length; |
+ page._frames.push({url: message.url, parent: parentFrame}); |
} |
- return {succeed: true, result: this.serialize(value, objects)}; |
- case "setProperty": |
- var obj = objects[message.objectId]; |
- var value = this.deserialize(message.value, objects, tab); |
- |
- try |
- { |
- obj[message.property] = value; |
- } |
- catch (e) |
- { |
- return this.fail(e); |
- } |
- |
- return {succeed: true}; |
- case "callFunction": |
- var func = objects[message.functionId]; |
- var context = objects[message.contextId]; |
- |
- var args = []; |
- for (var i = 0; i < message.args.length; i++) |
- args.push(this.deserialize(message.args[i], objects, tab)); |
- |
- try |
- { |
- var result = func.apply(context, args); |
- } |
- catch (e) |
- { |
- return this.fail(e); |
- } |
- |
- return {succeed: true, result: this.serialize(result, objects)}; |
- case "inspectObject": |
- var obj = objects[message.objectId]; |
- var objectInfo = {properties: {}, isFunction: typeof obj == "function"}; |
- |
- Object.getOwnPropertyNames(obj).forEach(function(prop) |
- { |
- objectInfo.properties[prop] = { |
- enumerable: Object.prototype.propertyIsEnumerable.call(obj, prop) |
- }; |
- }); |
- |
- if (obj.__proto__) |
- objectInfo.prototypeId = this.registerObject(obj.__proto__, objects); |
- |
- if (obj == Object.prototype) |
- objectInfo.prototypeOf = "Object"; |
- if (obj == Function.prototype) |
- objectInfo.prototypeOf = "Function"; |
- |
- return objectInfo; |
- } |
- } |
- }; |
- |
- |
- /* Web request blocking */ |
- |
- ext.webRequest = { |
- onBeforeRequest: { |
- _listeners: [], |
- _urlPatterns: [], |
- |
- _handleMessage: function(message, tab) |
- { |
- tab = new Tab(tab); |
- |
- for (var i = 0; i < this._listeners.length; i++) |
- { |
- var regex = this._urlPatterns[i]; |
- |
- if ((!regex || regex.test(message.url)) && this._listeners[i](message.url, message.type, tab, 0, -1) === false) |
- return false; |
- } |
- |
- return true; |
- }, |
- addListener: function(listener, urls) |
- { |
- var regex; |
- |
- if (urls) |
- regex = new RegExp("^(?:" + urls.map(function(url) |
- { |
- return url.split("*").map(function(s) |
- { |
- return s.replace(/([.?+^$[\]\\(){}|-])/g, "\\$1"); |
- }).join(".*"); |
- }).join("|") + ")($|[?#])"); |
- |
- this._listeners.push(listener); |
- this._urlPatterns.push(regex); |
- }, |
- removeListener: function(listener) |
- { |
- var idx = this._listeners.indexOf(listener); |
- |
- if (idx != -1) |
- { |
- this._listeners.splice(idx, 1); |
- this._urlPatterns.splice(idx, 1); |
- } |
- } |
- }, |
- handlerBehaviorChanged: function() {} |
- }; |
- |
- |
- /* Synchronous messaging */ |
- |
- safari.application.addEventListener("message", function(event) |
- { |
- if (event.name == "canLoad") |
- { |
- var handler; |
- |
- switch (event.message.type) |
- { |
- case "proxy": |
- handler = proxy; |
+ event.message = {pageId: pageId, frameId: frameId}; |
break; |
case "webRequest": |
- handler = ext.webRequest.onBeforeRequest; |
+ var page = pages[event.message.pageId]; |
+ |
+ event.message = ext.webRequest.onBeforeRequest._dispatch( |
+ event.message.url, |
+ event.message.type, |
+ page, |
+ page._frames[event.message.frameId] |
+ ); |
+ break; |
+ case "proxy": |
+ event.message = backgroundPageProxy.handleMessage(event.message); |
break; |
} |
+ break; |
+ case "request": |
+ var page = pages[event.message.pageId]; |
+ var sender = {page: page, frame: page._frames[event.message.frameId]}; |
+ page._messageProxy.handleRequest(event.message, sender); |
+ break; |
+ case "response": |
+ pages[event.message.pageId]._messageProxy.handleResponse(event.message); |
+ break; |
+ case "replaced": |
+ // when a prerendered page is shown, forget the previous page |
+ // associated with its tab, and reset the toolbar item if necessary. |
+ // Note that it wouldn't be sufficient to do that when the old |
+ // page is unloading, because Safari dispatches window.onunload |
+ // only when reloading the page or following links, but not when |
+ // the current page is replaced with a prerendered page. |
+ replacePage(pages[event.message.pageId]); |
+ break; |
+ } |
+ }); |
- event.message = handler._handleMessage(event.message.payload, event.target); |
- } |
- }, true); |
- } |
+ /* Storage */ |
- /* API */ |
- |
- ext.windows = { |
- getAll: function(callback) |
- { |
- callback(safari.application.browserWindows.map(function(win) |
- { |
- return new Window(win); |
- })); |
- }, |
- getLastFocused: function(callback) |
- { |
- callback(new Window(safari.application.activeBrowserWindow)); |
- } |
- }; |
- |
- ext.tabs = { |
- onLoading: new LoadingTabEventTarget(safari.application), |
- onCompleted: new TabEventTarget(safari.application, "navigate", true), |
- onActivated: new TabEventTarget(safari.application, "activate", true), |
- onRemoved: new TabEventTarget(safari.application, "close", true) |
- }; |
- |
- ext.backgroundPage = { |
- getWindow: function() |
- { |
- return safari.extension.globalPage.contentWindow; |
- } |
- }; |
- |
- ext.onMessage = new MessageEventTarget(safari.application); |
- |
- // TODO: Implement context menu |
- ext.contextMenus = { |
- create: function(title, contexts, onclick) {}, |
- removeAll: function(callback) {} |
- }; |
- |
- // Safari will load the bubble once, and then show it everytime the icon is |
- // clicked. While Chrome loads it everytime you click the icon. So in order to |
- // force the same behavior in Safari, we are going to reload the page of the |
- // bubble everytime it is shown. |
- if (safari.extension.globalPage.contentWindow != window) |
- safari.application.addEventListener("popover", function() |
- { |
- document.documentElement.style.display = "none"; |
- document.location.reload(); |
- }, true); |
+ ext.storage = safari.extension.settings; |
})(); |