| Index: background.js |
| =================================================================== |
| --- a/background.js |
| +++ b/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 |
| @@ -26,6 +26,13 @@ |
| { |
| this.Subscription = Subscription; |
| this.DownloadableSubscription = DownloadableSubscription; |
| + this.SpecialSubscription = SpecialSubscription; |
| +} |
| +with(require("whitelisting")) |
| +{ |
| + this.isWhitelisted = isWhitelisted; |
| + this.isFrameWhitelisted = isFrameWhitelisted; |
| + this.processKeyException = processKeyException; |
| } |
| var FilterStorage = require("filterStorage").FilterStorage; |
| var ElemHide = require("elemHide").ElemHide; |
| @@ -34,13 +41,20 @@ |
| var Synchronizer = require("synchronizer").Synchronizer; |
| var Utils = require("utils").Utils; |
| var Notification = require("notification").Notification; |
| +var initAntiAdblockNotification = require("antiadblockInit").initAntiAdblockNotification; |
| // Some types cannot be distinguished |
| RegExpFilter.typeMap.OBJECT_SUBREQUEST = RegExpFilter.typeMap.OBJECT; |
| RegExpFilter.typeMap.MEDIA = RegExpFilter.typeMap.FONT = RegExpFilter.typeMap.OTHER; |
| -var isFirstRun = false; |
| +// Chrome on Linux does not fully support chrome.notifications until version 35 |
| +// https://code.google.com/p/chromium/issues/detail?id=291485 |
| +var canUseChromeNotifications = require("info").platform == "chromium" |
| + && "notifications" in chrome |
| + && (navigator.platform.indexOf("Linux") == -1 || parseInt(require("info").applicationVersion) > 34); |
| + |
| var seenDataCorruption = false; |
| +var filterlistsReinitialized = false; |
| require("filterNotifier").FilterNotifier.addListener(function(action) |
| { |
| if (action == "load") |
| @@ -48,15 +62,32 @@ |
| var importingOldData = importOldData(); |
| var addonVersion = require("info").addonVersion; |
| - var prevVersion = localStorage.currentVersion; |
| - if (prevVersion != addonVersion) |
| + var prevVersion = ext.storage.currentVersion; |
| + |
| + // There are no filters stored so we need to reinitialize all filterlists |
| + if (!FilterStorage.firstRun && FilterStorage.subscriptions.length === 0) |
| { |
| - isFirstRun = !prevVersion; |
| - localStorage.currentVersion = addonVersion; |
| + filterlistsReinitialized = true; |
| + prevVersion = null; |
| + } |
| + |
| + if (prevVersion != addonVersion || FilterStorage.firstRun) |
| + { |
| + seenDataCorruption = prevVersion && FilterStorage.firstRun; |
| + ext.storage.currentVersion = addonVersion; |
| if (!importingOldData) |
| addSubscription(prevVersion); |
| } |
| + |
| + if (canUseChromeNotifications) |
| + initChromeNotifications(); |
| + initAntiAdblockNotification(); |
| } |
| + |
| + // update browser actions when whitelisting might have changed, |
| + // due to loading filters or saving filter changes |
| + if (action == "load" || action == "save") |
| + refreshIconAndContextMenuForAllPages(); |
| }); |
| // Special-case domains for which we cannot use style-based hiding rules. |
| @@ -68,76 +99,57 @@ |
| var deprecatedOptions = ["specialCaseYouTube", "experimental", "disableInlineTextAds"]; |
| deprecatedOptions.forEach(function(option) |
| { |
| - if (option in localStorage) |
| - delete localStorage[option]; |
| + if (option in ext.storage) |
| + delete ext.storage[option]; |
| }); |
| } |
| -// Sets options to defaults, upgrading old options from previous versions as necessary |
| -function setDefaultOptions() |
| -{ |
| - function defaultOptionValue(opt, val) |
| - { |
| - if(!(opt in localStorage)) |
| - localStorage[opt] = val; |
| - } |
| - |
| - defaultOptionValue("shouldShowBlockElementMenu", "true"); |
| - |
| - removeDeprecatedOptions(); |
| -} |
| - |
| -// Upgrade options before we do anything else. |
| -setDefaultOptions(); |
| - |
| -/** |
| - * Checks whether a page is whitelisted. |
| - * @param {String} url |
| - * @param {String} [parentUrl] URL of the parent frame |
| - * @param {String} [type] content type to be checked, default is "DOCUMENT" |
| - * @return {Filter} filter that matched the URL or null if not whitelisted |
| - */ |
| -function isWhitelisted(url, parentUrl, type) |
| -{ |
| - // Ignore fragment identifier |
| - var index = url.indexOf("#"); |
| - if (index >= 0) |
| - url = url.substring(0, index); |
| - |
| - var result = defaultMatcher.matchesAny(url, type || "DOCUMENT", extractHostFromURL(parentUrl || url), false); |
| - return (result instanceof WhitelistFilter ? result : null); |
| -} |
| +// Remove deprecated options before we do anything else. |
| +removeDeprecatedOptions(); |
| var activeNotification = null; |
| +var contextMenuItem = { |
| + title: ext.i18n.getMessage("block_element"), |
| + contexts: ["image", "video", "audio"], |
| + onclick: function(srcUrl, page) |
| + { |
| + if (srcUrl) |
| + page.sendMessage({type: "clickhide-new-filter", filter: srcUrl}); |
| + } |
| +}; |
| + |
| // Adds or removes browser action icon according to options. |
| -function refreshIconAndContextMenu(tab) |
| +function refreshIconAndContextMenu(page) |
| { |
| - if(!/^https?:/.test(tab.url)) |
| - return; |
| + var whitelisted = isWhitelisted(page.url); |
| var iconFilename; |
| - if (require("info").platform == "safari") |
| - // There is no grayscale version of the icon for whitelisted tabs |
| + if (whitelisted && require("info").platform != "safari") |
| + // There is no grayscale version of the icon for whitelisted pages |
| // when using Safari, because icons are grayscale already and icons |
| - // aren't per tab in Safari. |
| - iconFilename = "icons/abp-16.png" |
| + // aren't per page in Safari. |
| + iconFilename = "icons/abp-$size-whitelisted.png"; |
| else |
| + iconFilename = "icons/abp-$size.png"; |
| + |
| + page.browserAction.setIcon(iconFilename); |
| + iconAnimation.registerPage(page, iconFilename); |
| + |
| + // show or hide the context menu entry dependent on whether |
| + // adblocking is active on that page |
| + page.contextMenus.removeAll(); |
| + |
| + if (Prefs.shouldShowBlockElementMenu && !whitelisted && /^https?:/.test(page.url)) |
| + page.contextMenus.create(contextMenuItem); |
| +} |
| + |
| +function refreshIconAndContextMenuForAllPages() |
| +{ |
| + ext.pages.query({}, function(pages) |
| { |
| - var excluded = isWhitelisted(tab.url); |
| - iconFilename = excluded ? "icons/abp-19-whitelisted.png" : "icons/abp-19.png"; |
| - } |
| - |
| - tab.browserAction.setIcon(iconFilename); |
| - tab.browserAction.setTitle(ext.i18n.getMessage("name")); |
| - |
| - iconAnimation.registerTab(tab, iconFilename); |
| - |
| - // Set context menu status according to whether current tab has whitelisted domain |
| - if (excluded) |
| - chrome.contextMenus.removeAll(); |
| - else |
| - showContextMenu(); |
| + pages.forEach(refreshIconAndContextMenu); |
| + }); |
| } |
| /** |
| @@ -217,15 +229,25 @@ |
| addAcceptable = false; |
| } |
| + // Add "anti-adblock messages" subscription for new users and users updating from old ABP versions |
| + if (!prevVersion || Services.vc.compare(prevVersion, "1.8") < 0) |
| + { |
| + var subscription = Subscription.fromURL(Prefs.subscriptions_antiadblockurl); |
| + if (subscription && !(subscription.url in FilterStorage.knownSubscriptions)) |
| + { |
| + subscription.disabled = true; |
| + FilterStorage.addSubscription(subscription); |
| + if (subscription instanceof DownloadableSubscription && !subscription.lastDownload) |
| + Synchronizer.execute(subscription); |
| + } |
| + } |
| + |
| if (!addSubscription && !addAcceptable) |
| return; |
| function notifyUser() |
| { |
| - ext.windows.getLastFocused(function(win) |
| - { |
| - win.openTab(ext.getURL("firstRun.html")); |
| - }); |
| + ext.pages.open(ext.getURL("firstRun.html")); |
| } |
| if (addSubscription) |
| @@ -255,91 +277,234 @@ |
| notifyUser(); |
| } |
| -// Set up context menu for user selection of elements to block |
| -function showContextMenu() |
| +Prefs.addListener(function(name) |
| { |
| - ext.contextMenus.removeAll(function() |
| - { |
| - if(typeof localStorage["shouldShowBlockElementMenu"] == "string" && localStorage["shouldShowBlockElementMenu"] == "true") |
| - { |
| - ext.contextMenus.create(ext.i18n.getMessage("block_element"), ["image", "video", "audio"], function(srcUrl, tab) |
| - { |
| - if(srcUrl) |
| - tab.sendMessage({type: "clickhide-new-filter", filter: srcUrl}); |
| - }); |
| - } |
| - }); |
| -} |
| + if (name == "shouldShowBlockElementMenu") |
| + refreshIconAndContextMenuForAllPages(); |
| +}); |
| /** |
| - * Opens options tab or focuses an existing one, within the last focused window. |
| + * Opens options page or focuses an existing one, within the last focused window. |
| * @param {Function} callback function to be called with the |
| - Tab object of the options tab |
| + Page object of the options page |
| */ |
| function openOptions(callback) |
| { |
| - ext.windows.getLastFocused(function(win) |
| + ext.pages.query({lastFocusedWindow: true}, function(pages) |
| { |
| - win.getAllTabs(function(tabs) |
| + var optionsUrl = ext.getURL("options.html"); |
| + |
| + for (var i = 0; i < pages.length; i++) |
| { |
| - var optionsUrl = ext.getURL("options.html"); |
| + var page = pages[i]; |
| + if (page.url == optionsUrl) |
| + { |
| + page.activate(); |
| + if (callback) |
| + callback(page); |
| + return; |
| + } |
| + } |
| - for (var i = 0; i < tabs.length; i++) |
| - { |
| - if (tabs[i].url == optionsUrl) |
| - { |
| - tabs[i].activate(); |
| - if (callback) |
| - callback(tabs[i]); |
| - return; |
| - } |
| - } |
| - |
| - win.openTab(optionsUrl, callback && function(tab) |
| - { |
| - tab.onCompleted.addListener(callback); |
| - }); |
| - }); |
| + ext.pages.open(optionsUrl, callback); |
| }); |
| } |
| function prepareNotificationIconAndPopup() |
| { |
| + var animateIcon = (activeNotification.type !== "question"); |
| activeNotification.onClicked = function() |
| { |
| - iconAnimation.stop(); |
| - activeNotification = null; |
| + if (animateIcon) |
| + iconAnimation.stop(); |
| + notificationClosed(); |
| }; |
| + if (animateIcon) |
| + iconAnimation.update(activeNotification.type); |
| +} |
| - iconAnimation.update(activeNotification.severity); |
| +function openNotificationLinks() |
| +{ |
| + if (activeNotification.links) |
| + { |
| + activeNotification.links.forEach(function(link) |
| + { |
| + ext.windows.getLastFocused(function(win) |
| + { |
| + win.openTab(Utils.getDocLink(link)); |
| + }); |
| + }); |
| + } |
| +} |
| + |
| +function notificationButtonClick(buttonIndex) |
| +{ |
| + if (activeNotification.type === "question") |
| + { |
| + Notification.triggerQuestionListeners(activeNotification.id, buttonIndex === 0); |
| + Notification.markAsShown(activeNotification.id); |
| + activeNotification.onClicked(); |
| + } |
| + else if (activeNotification.links && activeNotification.links[buttonIndex]) |
| + { |
| + ext.windows.getLastFocused(function(win) |
| + { |
| + win.openTab(Utils.getDocLink(activeNotification.links[buttonIndex])); |
| + }); |
| + } |
| +} |
| + |
| +function notificationClosed() |
| +{ |
| + activeNotification = null; |
| +} |
| + |
| +function imgToBase64(url, callback) |
| +{ |
| + var canvas = document.createElement("canvas"), |
| + ctx = canvas.getContext("2d"), |
| + img = new Image; |
| + img.src = url; |
| + img.onload = function() |
| + { |
| + canvas.height = img.height; |
| + canvas.width = img.width; |
| + ctx.drawImage(img, 0, 0); |
| + callback(canvas.toDataURL("image/png")); |
| + canvas = null; |
| + }; |
| +} |
| + |
| +function initChromeNotifications() |
| +{ |
| + // Chrome hides notifications in notification center when clicked so we need to clear them |
| + function clearActiveNotification(notificationId) |
| + { |
| + if (activeNotification && activeNotification.type != "question" && !("links" in activeNotification)) |
| + return; |
| + |
| + chrome.notifications.clear(notificationId, function(wasCleared) |
| + { |
| + if (wasCleared) |
| + notificationClosed(); |
| + }); |
| + } |
| + |
| + chrome.notifications.onButtonClicked.addListener(function(notificationId, buttonIndex) |
| + { |
| + notificationButtonClick(buttonIndex); |
| + clearActiveNotification(notificationId); |
| + }); |
| + chrome.notifications.onClicked.addListener(clearActiveNotification); |
| + chrome.notifications.onClosed.addListener(notificationClosed); |
| } |
| function showNotification(notification) |
| { |
| + if (activeNotification && activeNotification.id === notification.id) |
| + return; |
| + |
| activeNotification = notification; |
| + if (activeNotification.type === "critical" || activeNotification.type === "question") |
| + { |
| + var hasWebkitNotifications = typeof webkitNotifications !== "undefined"; |
| + if (hasWebkitNotifications && "createHTMLNotification" in webkitNotifications) |
| + { |
| + var notification = webkitNotifications.createHTMLNotification("notification.html"); |
| + notification.show(); |
| + prepareNotificationIconAndPopup(); |
| + return; |
| + } |
| - if (activeNotification.severity === "critical" |
| - && typeof webkitNotifications !== "undefined") |
| - { |
| - var notification = webkitNotifications.createHTMLNotification("notification.html"); |
| - notification.show(); |
| - notification.addEventListener("close", prepareNotificationIconAndPopup); |
| + var texts = Notification.getLocalizedTexts(notification); |
| + var title = texts.title || ""; |
| + var message = texts.message ? texts.message.replace(/<\/?(a|strong)>/g, "") : ""; |
| + var iconUrl = ext.getURL("icons/abp-128.png"); |
| + var hasLinks = activeNotification.links && activeNotification.links.length > 0; |
| + |
| + if (canUseChromeNotifications) |
| + { |
| + var opts = { |
| + type: "basic", |
| + title: title, |
| + message: message, |
| + buttons: [], |
| + priority: 2 // We use the highest priority to prevent the notification from closing automatically |
| + }; |
| + if (activeNotification.type === "question") |
| + { |
| + opts.buttons.push({title: ext.i18n.getMessage("overlay_notification_button_yes")}); |
| + opts.buttons.push({title: ext.i18n.getMessage("overlay_notification_button_no")}); |
| + } |
| + else |
| + { |
| + var regex = /<a>(.*?)<\/a>/g; |
| + var plainMessage = texts.message || ""; |
| + var match; |
| + while (match = regex.exec(plainMessage)) |
| + opts.buttons.push({title: match[1]}); |
| + } |
| + |
| + imgToBase64(iconUrl, function(iconData) |
| + { |
| + opts["iconUrl"] = iconData; |
| + chrome.notifications.create("", opts, function() {}); |
| + }); |
| + } |
| + else if (hasWebkitNotifications && "createNotification" in webkitNotifications && activeNotification.type !== "question") |
| + { |
| + if (hasLinks) |
| + message += " " + ext.i18n.getMessage("notification_without_buttons"); |
| + |
| + imgToBase64(iconUrl, function(iconData) |
| + { |
| + var notification = webkitNotifications.createNotification(iconData, title, message); |
| + notification.show(); |
| + notification.addEventListener("click", openNotificationLinks, false); |
| + notification.addEventListener("close", notificationClosed, false); |
| + }); |
| + } |
| + else |
| + { |
| + var message = title + "\n" + message; |
| + if (hasLinks) |
| + message += "\n\n" + ext.i18n.getMessage("notification_with_buttons"); |
| + |
| + var approved = confirm(message); |
| + if (activeNotification.type === "question") |
| + notificationButtonClick(approved ? 0 : 1); |
| + else if (approved) |
| + openNotificationLinks(); |
| + } |
| } |
| - else |
| - prepareNotificationIconAndPopup(); |
| + prepareNotificationIconAndPopup(); |
| } |
| -/** |
| - * This function is a hack - we only know the tabId and document URL for a |
| - * message but we need to know the frame ID. Try to find it in webRequest"s |
| - * frame data. |
| - */ |
| -function getFrameId(tab, url) |
| +// This is a hack to speedup loading of the options page on Safari. |
| +// Once we replaced the background page proxy with message passing |
| +// this global function should removed. |
| +function getUserFilters() |
| { |
| - for (var frameId in frames.get(tab)) |
| - if (getFrameUrl(tab, frameId) == url) |
| - return frameId; |
| - return -1; |
| + var filters = []; |
| + var exceptions = []; |
| + |
| + for (var i = 0; i < FilterStorage.subscriptions.length; i++) |
| + { |
| + var subscription = FilterStorage.subscriptions[i]; |
| + if (!(subscription instanceof SpecialSubscription)) |
| + continue; |
| + |
| + for (var j = 0; j < subscription.filters.length; j++) |
| + { |
| + var filter = subscription.filters[j]; |
| + if (filter instanceof WhitelistFilter && /^@@\|\|([^\/:]+)\^\$document$/.test(filter.text)) |
| + exceptions.push(RegExp.$1); |
| + else |
| + filters.push(filter.text); |
| + } |
| + } |
| + |
| + return {filters: filters, exceptions: exceptions}; |
| } |
| ext.onMessage.addListener(function (msg, sender, sendResponse) |
| @@ -347,14 +512,13 @@ |
| switch (msg.type) |
| { |
| case "get-selectors": |
| - var selectors = null; |
| - var frameId = sender.tab ? getFrameId(sender.tab, msg.frameUrl) : -1; |
| + var selectors = []; |
| - if (!isFrameWhitelisted(sender.tab, frameId, "DOCUMENT") && |
| - !isFrameWhitelisted(sender.tab, frameId, "ELEMHIDE")) |
| + if (!isFrameWhitelisted(sender.page, sender.frame, "DOCUMENT") && |
| + !isFrameWhitelisted(sender.page, sender.frame, "ELEMHIDE")) |
| { |
| var noStyleRules = false; |
| - var host = extractHostFromURL(msg.frameUrl); |
| + var host = extractHostFromFrame(sender.frame); |
| for (var i = 0; i < noStyleRulesHosts.length; i++) |
| { |
| var noStyleHost = noStyleRulesHosts[i]; |
| @@ -377,23 +541,21 @@ |
| sendResponse(selectors); |
| break; |
| case "should-collapse": |
| - var frameId = sender.tab ? getFrameId(sender.tab, msg.documentUrl) : -1; |
| - |
| - if (isFrameWhitelisted(sender.tab, frameId, "DOCUMENT")) |
| + if (isFrameWhitelisted(sender.page, sender.frame, "DOCUMENT")) |
| { |
| sendResponse(false); |
| break; |
| } |
| var requestHost = extractHostFromURL(msg.url); |
| - var documentHost = extractHostFromURL(msg.documentUrl); |
| + var documentHost = extractHostFromFrame(sender.frame); |
| var thirdParty = isThirdParty(requestHost, documentHost); |
| var filter = defaultMatcher.matchesAny(msg.url, msg.mediatype, documentHost, thirdParty); |
| if (filter instanceof BlockingFilter) |
| { |
| var collapse = filter.collapse; |
| if (collapse == null) |
| - collapse = (localStorage.hidePlaceholders != "false"); |
| + collapse = Prefs.hidePlaceholders; |
| sendResponse(collapse); |
| } |
| else |
| @@ -402,9 +564,9 @@ |
| case "get-domain-enabled-state": |
| // Returns whether this domain is in the exclusion list. |
| // The browser action popup asks us this. |
| - if(sender.tab) |
| + if(sender.page) |
| { |
| - sendResponse({enabled: !isWhitelisted(sender.tab.url)}); |
| + sendResponse({enabled: !isWhitelisted(sender.page.url)}); |
| return; |
| } |
| break; |
| @@ -416,15 +578,18 @@ |
| } |
| break; |
| case "add-subscription": |
| - openOptions(function(tab) |
| + openOptions(function(page) |
| { |
| - tab.sendMessage(msg); |
| + page.sendMessage(msg); |
| }); |
| break; |
| + case "add-key-exception": |
| + processKeyException(msg.token, sender.page, sender.frame); |
| + break; |
| case "forward": |
| - if (sender.tab) |
| + if (sender.page) |
| { |
| - sender.tab.sendMessage(msg.payload, sendResponse); |
| + sender.page.sendMessage(msg.payload, sendResponse); |
| // Return true to indicate that we want to call |
| // sendResponse asynchronously |
| return true; |
| @@ -436,23 +601,11 @@ |
| } |
| }); |
| -// Show icon as browser action for all tabs that already exist |
| -ext.windows.getAll(function(windows) |
| +// update icon when page changes location |
| +ext.pages.onLoading.addListener(function(page) |
| { |
| - for (var i = 0; i < windows.length; i++) |
| - { |
| - windows[i].getAllTabs(function(tabs) |
| - { |
| - tabs.forEach(refreshIconAndContextMenu); |
| - }); |
| - } |
| -}); |
| - |
| -// Update icon if a tab changes location |
| -ext.tabs.onLoading.addListener(function(tab) |
| -{ |
| - tab.sendMessage({type: "clickhide-deactivate"}); |
| - refreshIconAndContextMenu(tab); |
| + page.sendMessage({type: "clickhide-deactivate"}); |
| + refreshIconAndContextMenu(page); |
| }); |
| setTimeout(function() |