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