diff --git a/src/js/vapi-background.js b/src/js/vapi-background.js new file mode 100644 index 0000000..1db13ae --- /dev/null +++ b/src/js/vapi-background.js @@ -0,0 +1,848 @@ +// for background page only + +(function() { +'use strict'; + +window.vAPI = window.vAPI || {}; + +if (window.chrome) { + var chrome = window.chrome; + + vAPI.chrome = true; + + vAPI.storage = chrome.storage.local; + + vAPI.tabs = { + registerListeners: function() { + if (typeof this.onNavigation === 'function') { + chrome.webNavigation.onCommitted.addListener(this.onNavigation); + } + + if (typeof this.onUpdated === 'function') { + chrome.tabs.onUpdated.addListener(this.onUpdated); + } + + if (typeof this.onClosed === 'function') { + chrome.tabs.onRemoved.addListener(this.onClosed); + } + + if (typeof this.onPopup === 'function') { + chrome.webNavigation.onCreatedNavigationTarget.addListener(this.onPopup); + } + }, + get: function(tabId, callback) { + if (tabId === null) { + chrome.tabs.query( + { + active: true, + currentWindow: true + }, + function(tabs) { + callback(tabs[0]); + } + ); + } + else { + chrome.tabs.get(tabId, callback); + } + }, + /*open: function(details) { + // to keep incognito context? + chrome.windows.getCurrent(function(win) { + details.windowId = win.windowId; + chrome.tabs.create(details); + }); + },*/ + open: function(details) { + if (!details.url) { + return null; + } + // extension pages + else if (!details.url.match(/^\w{2,20}:/)) { + details.url = vAPI.getURL(details.url); + } + + // dealing with Chrome's asynhronous API + var wrapper = function() { + if (details.active === undefined) { + details.active = true; + } + + var subWrapper = function() { + var _details = { + url: details.url, + active: !!details.active + }; + + if (details.tabId) { + // update doesn't accept index, must use move + chrome.tabs.update(details.tabId, _details, function(tab) { + // if the tab doesn't exist + if (chrome.runtime.lastError) { + chrome.tabs.create(_details); + } + else if (details.index !== undefined) { + chrome.tabs.move(tab.id, {index: details.index}); + } + }); + } + else { + if (details.index !== undefined) { + _details.index = details.index; + } + + chrome.tabs.create(_details); + } + }; + + if (details.index === -1) { + vAPI.tabs.get(null, function(tab) { + if (tab) { + details.index = tab.index + 1; + } + else { + delete details.index; + } + + subWrapper(); + }); + } + else { + subWrapper(); + } + }; + + if (details.select) { + // note that currentWindow may be even the window of Developer Tools + // so, test with setTimeout... + chrome.tabs.query({currentWindow: true}, function(tabs) { + var url = details.url.replace(rgxHash, ''); + // this is questionable + var rgxHash = /#.*/; + + tabs = tabs.some(function(tab) { + if (tab.url.replace(rgxHash, '') === url) { + chrome.tabs.update(tab.id, {active: true}); + return true; + } + }); + + if (!tabs) { + wrapper(); + } + }); + } + else { + wrapper(); + } + }, + close: chrome.tabs.remove.bind(chrome.tabs) + }; + + // Must read: https://code.google.com/p/chromium/issues/detail?id=410868#c8 + + // https://github.com/gorhill/uBlock/issues/19 + // https://github.com/gorhill/uBlock/issues/207 + // Since we may be called asynchronously, the tab id may not exist + // anymore, so this ensures it does still exist. + + vAPI.setIcon = function(tabId, img, badge) { + var onIconReady = function() { + if ( chrome.runtime.lastError ) { + return; + } + + chrome.browserAction.setBadgeText({ tabId: tabId, text: badge }); + + if ( badge !== '' ) { + chrome.browserAction.setBadgeBackgroundColor({ tabId: tabId, color: '#666' }); + } + }; + chrome.browserAction.setIcon({ tabId: tabId, path: img }, onIconReady); + }; + + vAPI.messaging = { + ports: {}, + listeners: {}, + listen: function(name, callback) { + this.listeners[name] = callback; + }, + setup: function(connector) { + if (this.connector) { + return; + } + + this.connector = function(port) { + var onMessage = function(request) { + var callback = function(response) { + // stfu + if (chrome.runtime.lastError) { + return; + } + + if (request.requestId) { + port.postMessage({ + requestId: request.requestId, + portName: request.portName, + msg: response + }); + } + }; + + var listener = connector(request.msg, port.sender, callback); + + if (listener === null) { + listener = vAPI.messaging.listeners[request.portName]; + + if (typeof listener === 'function') { + listener(request.msg, port.sender, callback); + } else { + console.error('µBlock> messaging > unknown request: %o', request); + } + } + }; + + var onDisconnect = function(port) { + port.onDisconnect.removeListener(onDisconnect); + port.onMessage.removeListener(onMessage); + delete vAPI.messaging.ports[port.name]; + }; + + port.onDisconnect.addListener(onDisconnect); + port.onMessage.addListener(onMessage); + vAPI.messaging.ports[port.name] = port; + }; + + chrome.runtime.onConnect.addListener(this.connector); + }, + broadcast: function(message) { + message = { + broadcast: true, + msg: message + }; + + for (var portName in this.ports) { + this.ports[portName].postMessage(message); + } + } + }; + + vAPI.net = { + registerListeners: function() { + var listeners = [ + 'onBeforeRequest', + 'onBeforeSendHeaders', + 'onHeadersReceived' + ]; + + for (var i = 0; i < listeners.length; ++i) { + chrome.webRequest[listeners[i]].addListener( + this[listeners[i]].callback, + { + 'urls': this[listeners[i]].urls || [''], + 'types': this[listeners[i]].types || [] + }, + this[listeners[i]].extra + ); + } + } + }; + + vAPI.contextMenu = { + create: function(details, callback) { + this.menuId = details.id; + this.callback = callback; + chrome.contextMenus.create(details); + chrome.contextMenus.onClicked.addListener(this.callback); + }, + remove: function() { + chrome.contextMenus.onClicked.removeListener(this.callback); + chrome.contextMenus.remove(this.menuId); + } + }; +} else if (window.safari) { + vAPI.safari = true; + + safari.extension.settings.addEventListener('change', function(e) { + if (e.key === 'open_prefs') { + vAPI.tabs.open({url: 'dashboard.html', active: true}); + } + }, false); + + vAPI.storage = { + _storage: safari.extension.settings, + QUOTA_BYTES: 50 * 1024 * 1024, + get: function(keys, callback) { + if (typeof callback !== 'function') { + return; + } + + var i, value, result = {}; + + if (keys === null) { + for (i in this._storage) { + value = this._storage[i]; + + if (typeof value === 'string') { + result[i] = JSON.parse(value); + } + } + } + else if (typeof keys === 'string') { + value = this._storage[keys]; + + if (typeof value === 'string') { + result[keys] = JSON.parse(value); + } + } + else if (Array.isArray(keys)) { + for ( i = 0; i < keys.length; ++i) { + value = this._storage[i]; + + if (typeof value === 'string') { + result[keys[i]] = JSON.parse(value); + } + } + } + else if (typeof keys === 'object') { + for (i in keys) { + value = this._storage[i]; + + if (typeof value === 'string') { + result[i] = JSON.parse(value); + } + else { + result[i] = keys[i]; + } + } + } + + callback(result); + }, + set: function(details, callback) { + for (var key in details) { + this._storage.setItem(key, JSON.stringify(details[key])); + } + + if (typeof callback === 'function') { + callback(); + } + }, + remove: function(keys) { + if (typeof keys === 'string') { + keys = [keys]; + } + + for (var i = 0; i < keys.length; ++i) { + this._storage.removeItem(keys[i]); + } + }, + clear: function(callback) { + this._storage.clear(); + callback(); + }, + getBytesInUse: function(keys, callback) { + var key, size = 0; + + if (keys === null) { + for (key in this._storage) { + size += (this._storage[key] || '').length; + } + } + else { + if (typeof keys === 'string') { + keys = [keys]; + } + + for (key = 0; key < keys.length; ++key) { + size += (this._storage[keys[key]] || '').length; + } + } + + callback(size); + } + }; + + vAPI.tabs = { + stack: {}, + stackID: 1, + registerListeners: function() { + var onNavigation = this.onNavigation; + + if (typeof onNavigation === 'function') { + this.onNavigation = function(e) { + // e.url is not present for local files or data URIs + if (!e.target || !e.target.url) { + return; + } + + onNavigation({ + frameId: 0, + tabId: vAPI.tabs.getTabId(e.target), + url: e.target.url + }); + }; + + safari.application.addEventListener('navigate', this.onNavigation, true); + } + + // ?? + /*if (typeof onUpdated === 'function') { + chrome.tabs.onUpdated.addListener(this.onUpdated); + }*/ + + // onClosed handled in the main tab-close event + + // maybe intercept window.open on web-pages? + /*if (typeof onPopup === 'function') { + chrome.webNavigation.onCreatedNavigationTarget.addListener(this.onPopup); + }*/ + }, + getTabId: function(tab) { + for (var i in vAPI.tabs.stack) { + if (vAPI.tabs.stack[i] === tab) { + return +i; + } + } + + return -1; + }, + get: function(tabId, callback) { + var tab; + + if (tabId === null) { + tab = safari.application.activeBrowserWindow.activeTab; + tabId = this.getTabId(tab); + } + else { + tab = this.stack[tabId]; + } + + if (!tab) { + callback(); + return; + } + + callback({ + id: tabId, + index: tab.browserWindow.tabs.indexOf(tab), + windowId: safari.application.browserWindows.indexOf(tab.browserWindow), + active: tab === tab.browserWindow.activeTab, + url: tab.url, + title: tab.title + }); + }, + open: function(details) { + if (!details.url) { + return null; + } + // extension pages + else if (!details.url.match(/^\w{2,20}:/)) { + details.url = vAPI.getURL(details.url); + } + + // properties of the details object: + // url: 'URL', // the address that will be opened + // tabId: 1, // the tab is used if set, instead of creating a new one + // index: -1, // undefined: end of the list, -1: following tab, or after index + // active: false, // opens the tab in background - true and undefined: foreground + // select: true // if a tab is already opened with that url, then select it instead of opening a new one + + var curWin, tab; + + if (details.select) { + tab = safari.application.browserWindows.some(function(win) { + var rgxHash = /#.*/; + // this is questionable + var url = details.url.replace(rgxHash, ''); + + for (var i = 0; i < win.tabs.length; ++i) { + if (win.tabs[i].url.replace(rgxHash, '') === url) { + win.tabs[i].activate(); + return true; + } + } + }); + + if (tab) { + return; + } + } + + if (details.active === undefined) { + details.active = true; + } + + curWin = safari.application.activeBrowserWindow; + + // it must be calculated before opening a new tab, + // otherwise the new tab will be the active tab here + if (details.index === -1) { + details.index = curWin.tabs.indexOf(curWin.activeTab) + 1; + } + + tab = details.tabId && this.stack[details.tabId] + || curWin.openTab(details.active ? 'foreground' : 'background'); + + if (details.index !== undefined) { + curWin.insertTab(tab, details.index); + } + + tab.url = details.url; + }, + close: function(tab) { + if (!(tab instanceof SafariBrowserTab)) { + tab = this.stack[tab]; + } + + if (tab) { + tab.close(); + } + } + }; + + + // bind tabs to unique IDs + (function() { + var wins = safari.application.browserWindows, i = wins.length, j; + var tabs = []; + + while (i--) { + j = wins[i].tabs.length; + + while (j--) { + tabs.push(wins[i].tabs[j]); + } + } + + return tabs; + })().forEach(function(tab) { + vAPI.tabs.stack[vAPI.tabs.stackID++] = tab; + }); + + safari.application.addEventListener('open', function(e) { + // ignore windows + if (e.target instanceof SafariBrowserTab) { + vAPI.tabs.stack[vAPI.tabs.stackID++] = e.target; + } + }, true); + + safari.application.addEventListener('close', function(e) { + // ignore windows + if (!(e.target instanceof SafariBrowserTab)) { + return; + } + + var tabId = vAPI.tabs.getTabId(e.target); + + if (tabId > -1) { + // to not add another listener, put this here + // instead of vAPI.tabs.registerListeners + if (typeof vAPI.tabs.onClosed === 'function') { + vAPI.tabs.onClosed(tabId); + } + + delete vAPI.tabIcons[tabId]; + delete vAPI.tabs.stack[tabId]; + } + }, true); + + + // update badge when tab is activated + safari.application.addEventListener('activate', function(e) { + // hide popover, since in some cases won't close by itself + var items = safari.extension.toolbarItems; + + for (var i = 0; i < items.length; ++i) { + if (items[i].browserWindow === safari.application.activeBrowserWindow) { + if (items[i].popover) { + items[i].popover.hide(); + } + + break; + } + } + + // ignore windows + if (!(e.target instanceof SafariBrowserTab)) { + return; + } + + // update the badge, when tab is selected + vAPI.setIcon(); + }, true); + + // reload the popup when that is opened + safari.application.addEventListener('popover', function(e) { + e.target.contentWindow.document.body.textContent = ''; + e.target.contentWindow.location.reload(); + }, true); + + vAPI.tabIcons = { /*tabId: {badge: 0, img: dict}*/ }; + vAPI.setIcon = function(tabId, img, badge) { + var curTabId = vAPI.tabs.getTabId(safari.application.activeBrowserWindow.activeTab); + + // from 'activate' event + if (tabId === undefined) { + tabId = curTabId; + } + else { + vAPI.tabIcons[tabId] = { + badge: badge || 0/*, + img: img*/ + }; + } + + // if the selected tab has the same ID, then update the badge too, + // or always update it when changing tabs ('activate' event) + if (tabId === curTabId) { + var items = safari.extension.toolbarItems, i = items.length; + + while (i--) { + if (items[i].browserWindow === safari.application.activeBrowserWindow) { + if (vAPI.tabIcons[tabId]) { + items[i].badge = vAPI.tabIcons[tabId].badge; + // items[i].img = vAPI.tabIcons[tabId].img; + } + else { + items[i].badge = 0; + } + + return; + } + } + } + }; + + vAPI.messaging = { + listeners: {}, + listen: function(name, callback) { + this.listeners[name] = callback; + }, + setup: function(connector) { + if (this.connector) { + return; + } + + this.connector = function(request) { + if (request.name === 'canLoad') { + return; + } + + var callback = function(response) { + if (request.message.requestId) { + request.target.page.dispatchMessage( + 'message', + { + requestId: request.message.requestId, + portName: request.message.portName, + msg: response + } + ); + } + }; + + var sender = { + tab: { + id: vAPI.tabs.getTabId(request.target) + } + }; + + var listener = connector(request.message.msg, sender, callback); + + if (listener === null) { + listener = vAPI.messaging.listeners[request.message.portName]; + + if (typeof listener === 'function') { + listener(request.message.msg, sender, callback); + } else { + console.error('µBlock> messaging > unknown request: %o', request.message); + } + } + }; + + safari.application.addEventListener('message', this.connector, false); + }, + broadcast: function(message) { + message = { + broadcast: true, + msg: message + }; + + for (var tabId in vAPI.tabs.stack) { + vAPI.tabs.stack[tabId].page.dispatchMessage('message', message); + } + } + }; + + vAPI.net = { + registerListeners: function() { + // onBeforeRequest is used in the messaging above, in the connector method + // in order to use only one listener + var onBeforeRequest = this.onBeforeRequest; + + if (typeof onBeforeRequest.callback === 'function') { + if (!Array.isArray(onBeforeRequest.types)) { + onBeforeRequest.types = []; + } + + onBeforeRequest = onBeforeRequest.callback; + this.onBeforeRequest.callback = function(request) { + if (request.name !== 'canLoad') { + return; + } + + // no stopPropagation if it was called from beforeNavigate event + if (request.stopPropagation) { + request.stopPropagation(); + } + + var block = vAPI.net.onBeforeRequest; + + if (block.types.indexOf(request.message.type) < 0) { + return; + } + + request.message.tabId = vAPI.tabs.getTabId(request.target); + block = onBeforeRequest(request.message); + + // truthy return value will allow the request, + // except when redirectUrl is present + if (block && typeof block === 'object') { + if (block.cancel) { + request.message = false; + } + else if (typeof block.redirectUrl === "string") { + request.message = block.redirectUrl; + } + else { + request.message = true; + } + } + else { + request.message = true; + } + + return request.message; + }; + safari.application.addEventListener('message', this.onBeforeRequest.callback, true); + + // 'main_frame' simulation, since this isn't available in beforeload + safari.application.addEventListener('beforeNavigate', function(e) { + // e.url is not present for local files or data URIs + if (!e.url) { + return; + } + + vAPI.net.onBeforeRequest.callback({ + name: 'canLoad', + target: e.target, + message: { + url: e.url, + type: 'main_frame', + frameId: 0, + parentFrameId: -1, + timeStamp: e.timeStamp + } + }) || e.preventDefault(); + }, true); + } + } + }; + + vAPI.contextMenu = { + create: function(details, callback) { + var contexts = details.contexts; + var menuItemId = details.id; + var menuTitle = details.title; + + if (Array.isArray(contexts) && contexts.length) { + contexts = contexts.indexOf('all') === -1 ? contexts : null; + } + else { + // default in Chrome + contexts = ['page']; + } + + this.onContextMenu = function(e) { + var uI = e.userInfo; + + if (uI && /^https?:\/\//i.test(uI.pageUrl)) { + if (contexts) { + var invalidContext = true; + + for (var i = 0; i < contexts.length; ++i) { + if (contexts[i] === 'frame') { + if (uI.insideFrame) { + invalidContext = false; + break; + } + } + else if (contexts[i] === 'link') { + if (uI.linkHref) { + invalidContext = false; + break; + } + } + else if (contexts[i] === 'image') { + if (uI.srcUrl) { + invalidContext = false; + break; + } + } + else if (contexts[i] === 'audio' || contexts[i] === 'video') { + if (uI.srcUrl && uI.tagName === contexts[i]) { + invalidContext = false; + break; + } + } + else if (contexts[i] === 'editable') { + if (uI.editable) { + invalidContext = false; + break; + } + } + else if (contexts[i] === 'page') { + if (!(uI.insideFrame || uI.linkHref || uI.mediaType || uI.editable)) { + invalidContext = false; + break; + } + } + } + + if (invalidContext) { + return; + } + } + + e.contextMenu.appendContextMenuItem(menuItemId, menuTitle); + } + }; + + this.onContextMenuCommand = function(e) { + if (e.command === menuItemId) { + var tab = e.currentTarget.activeBrowserWindow.activeTab; + e.userInfo.menuItemId = menuItemId; + callback(e.userInfo, tab ? { + id: vAPI.tabs.getTabId(tab), + url: tab.url + } : undefined); + } + }; + + safari.application.addEventListener('contextmenu', this.onContextMenu); + safari.application.addEventListener("command", this.onContextMenuCommand); + }, + remove: function(argument) { + safari.application.removeEventListener('contextmenu', this.onContextMenu); + safari.application.removeEventListener("command", this.onContextMenuCommand); + this.onContextMenu = null; + this.onContextMenuCommand = null; + } + }; +} + +if (!window.chrome) { + window.chrome = { runtime: { lastError: null } }; +} +})();