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