/*
* This file is part of Adblock Plus ,
* Copyright (C) 2006-2017 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
* published by the Free Software Foundation.
*
* Adblock Plus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Adblock Plus. If not, see .
*/
/**
* @fileOverview Various application-specific functions.
*/
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AddonManager.jsm");
/**
* Checks whether an application window is known and should get Adblock Plus
* user interface elements.
* @result Boolean
*/
exports.isKnownWindow = (/**Window*/ window) => false;
/**
* HACK: In some applications the window finishes initialization during load
* event processing which makes an additional delay necessary. This flag
* indicates that.
* @type Boolean
*/
exports.delayInitialization = false;
/**
* Retrieves the browser element for an application window.
* @type function(window)
*/
exports.getBrowser = null;
/**
* Adds a new browser tab in the given application window.
* @type function(window, url, event)
*/
exports.addTab = null;
/**
* Retrieves the current browser location for an application window.
*/
exports.getCurrentLocation = function getCurrentLocation(/**Window*/ window) /**nsIURI|String*/
{
let browser = (exports.getBrowser ? exports.getBrowser(window) : null);
return (browser ? browser.currentURI : null);
}
/**
* The ID (or a list of possible IDs) of the content area context menu.
* @type String|String[]
*/
exports.contentContextMenu = null;
/**
* Determines the default placement of the toolbar icon via object properties
* parent, before and after.
* @type Object
*/
exports.defaultToolbarPosition = null;
/**
* The properties parent, before, after determine the placement of the status
* bar icon.
* @type Object
*/
exports.statusbarPosition = null;
/**
* The properties parent, before, after determine the placement of the Tools
* submenu.
* @type Object
*/
exports.toolsMenu = null;
/**
* Maps windows to their bottom bar info.
*/
let bottomBars = new WeakMap();
/**
* Adds a bottom bar to the application window.
* @type function(window, element)
*/
exports.addBottomBar = null;
function _addBottomBar(window, parent, element)
{
if (bottomBars.has(window) || !parent)
return null;
let bar = {elements: []};
for (let child = element.firstElementChild; child; child = child.nextElementSibling)
{
let clone = child.cloneNode(true);
parent.appendChild(clone);
bar.elements.push(clone);
}
bottomBars.set(window, bar);
return bar;
};
/**
* Removes the bottom bar from the application window.
* @type function(window)
*/
exports.removeBottomBar = null;
function _removeBottomBar(window)
{
if (!bottomBars.has(window))
return null;
let bar = bottomBars.get(window);
for (let i = 0; i < bar.elements.length; i++)
if (bar.elements[i].parentNode)
bar.elements[i].parentNode.removeChild(bar.elements[i]);
bottomBars.delete(window);
return bar;
};
/**
* Maps windows to a list of progress listeners.
*/
let progressListeners = new WeakMap();
/**
* Makes sure that a function is called whenever the displayed browser location changes.
*/
exports.addBrowserLocationListener = function addBrowserLocationListener(/**Window*/ window, /**Function*/ callback, /**Boolean*/ ignoreSameDoc)
{
let browser = (exports.getBrowser ? exports.getBrowser(window) : null);
if (browser)
{
let dummy = function() {};
let progressListener =
{
callback: callback,
onLocationChange: function(progress, request, uri, flags)
{
if (!ignoreSameDoc || !flags || !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT))
this.callback();
},
onProgressChange: dummy,
onSecurityChange: dummy,
onStateChange: dummy,
onStatusChange: dummy,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference])
};
browser.addProgressListener(progressListener);
if (progressListeners.has(window))
progressListeners.get(window).push(progressListener);
else
progressListeners.set(window, [progressListener]);
}
};
/**
* Removes a location listener registered for a window.
*/
exports.removeBrowserLocationListener = function removeBrowserLocationListener(/**Window*/ window, /**Function*/ callback)
{
if (!progressListeners.has(window))
return;
let browser = (exports.getBrowser ? exports.getBrowser(window) : null);
let listeners = progressListeners.get(window);
for (let i = 0; i < listeners.length; i++)
{
if (listeners[i].callback == callback)
{
if (browser)
browser.removeProgressListener(listeners[i]);
listeners.splice(i--, 1);
}
}
};
/**
* Removes all location listeners registered for a window, to be called on
* cleanup.
*/
exports.removeBrowserLocationListeners = function removeBrowserLocationListeners(/**Window*/ window)
{
if (!progressListeners.has(window))
return;
let browser = (exports.getBrowser ? exports.getBrowser(window) : null);
if (browser)
{
let listeners = progressListeners.get(window);
for (let i = 0; i < listeners.length; i++)
browser.removeProgressListener(listeners[i]);
}
progressListeners.delete(window);
};
let {application} = require("info");
switch (application)
{
case "firefox":
{
exports.isKnownWindow = function ff_isKnownWindow(window)
{
return (window.document.documentElement.getAttribute("windowtype") == "navigator:browser");
};
exports.getBrowser = (window) => window.gBrowser;
exports.addTab = function ff_addTab(window, url, event)
{
if (event)
window.openNewTabWith(url, null, null, event, false);
else
window.gBrowser.loadOneTab(url, {inBackground: false});
};
exports.contentContextMenu = "contentAreaContextMenu";
exports.defaultToolbarPosition = {
parent: "nav-bar"
};
exports.toolsMenu = {
parent: "menu_ToolsPopup"
};
exports.addBottomBar = function fx_addBottomBar(window, element)
{
let bar = _addBottomBar(window, window.document.getElementById("appcontent"), element);
if (bar)
{
let display = window.document.getElementById("statusbar-display");
bar.changedFixed = display && !display.hasAttribute("fixed");
if (bar.changedFixed)
display.setAttribute("fixed", "true");
}
};
exports.removeBottomBar = function fx_removeBottomBar(window)
{
let bar = _removeBottomBar(window);
if (bar && bar.changedFixed)
window.document.getElementById("statusbar-display").removeAttribute("fixed");
};
break;
}
case "seamonkey":
{
exports.isKnownWindow = function sm_isKnownWindow(window)
{
let type = window.document.documentElement.getAttribute("windowtype");
return (type == "navigator:browser" || type == "mail:3pane" || type == "mail:messageWindow");
};
exports.addTab = function sm_addTab(window, url, event)
{
if (event || !("gBrowser" in window))
window.openNewTabWith(url, null, null, event, false);
else
window.gBrowser.loadOneTab(url, {inBackground: false});
};
exports.getBrowser = function sm_getBrowser(window)
{
if ("gBrowser" in window)
return window.gBrowser;
else if ("getMessageBrowser" in window)
return window.getMessageBrowser();
else
return null;
};
exports.getCurrentLocation = function sm_getCurrentLocation(window)
{
if ("currentHeaderData" in window && "content-base" in window.currentHeaderData)
{
// This is a blog entry
return window.currentHeaderData["content-base"].headerValue;
}
else if ("currentHeaderData" in window && "from" in window.currentHeaderData)
{
// This is a mail/newsgroup entry
try
{
let headerParser = Cc["@mozilla.org/messenger/headerparser;1"].getService(Ci.nsIMsgHeaderParser);
let emailAddress = headerParser.extractHeaderAddressMailboxes(window.currentHeaderData.from.headerValue);
return "mailto:" + emailAddress.replace(/^[\s"]+/, "").replace(/[\s"]+$/, "").replace(/\s/g, "%20");
}
catch(e)
{
return null;
}
}
else
{
let browser = exports.getBrowser(window);
return (browser ? browser.currentURI : null);
}
};
// for Seamonkey we have to ignore same document flag because of
// bug #1035171 (https://bugzilla.mozilla.org/show_bug.cgi?id=1035171)
let origAddBrowserLocationListener = exports.addBrowserLocationListener;
exports.addBrowserLocationListener = function sm_addBrowserLocationListener(window, callback, ignoreSameDoc)
{
origAddBrowserLocationListener(window, callback, false);
};
exports.contentContextMenu = ["contentAreaContextMenu", "mailContext"];
exports.defaultToolbarPosition = {
parent: ["PersonalToolbar", "msgToolbar"],
before: ["bookmarks-button", "button-junk"]
};
exports.statusbarPosition = {
parent: "status-bar"
};
exports.toolsMenu = {
parent: "taskPopup",
after: "downloadmgr"
};
exports.addBottomBar = function sm_addBottomBar(window, element)
{
_addBottomBar(window, window.document.getElementById("appcontent") || window.document.getElementById("messagepanebox"), element);
};
exports.removeBottomBar = _removeBottomBar;
break;
}
case "thunderbird":
{
exports.isKnownWindow = function tb_isKnownWindow(window)
{
let type = window.document.documentElement.getAttribute("windowtype");
return (type == "mail:3pane" || type == "mail:messageWindow");
};
exports.delayInitialization = true;
exports.getBrowser = (window) => window.getBrowser();
exports.addTab = function tb_addTab(window, url, event)
{
let tabmail = window.document.getElementById("tabmail");
if (!tabmail)
{
let wnd = Services.wm.getMostRecentWindow("mail:3pane");
if (window)
tabmail = wnd.document.getElementById("tabmail");
}
if (tabmail)
tabmail.openTab("contentTab", {contentPage: url});
else
{
window.openDialog("chrome://messenger/content/", "_blank",
"chrome,dialog=no,all", null,
{
tabType: "contentTab",
tabParams: {contentPage: url}
});
}
};
exports.contentContextMenu = ["mailContext", "pageContextMenu"];
exports.defaultToolbarPosition = {
parent: "header-view-toolbar",
before: "hdrReplyButton",
addClass: "msgHeaderView-button"
};
exports.statusbarPosition = {
parent: "status-bar"
};
exports.toolsMenu = {
parent: "taskPopup",
after: "javaScriptConsole"
};
exports.getCurrentLocation = function getCurrentLocation(window)
{
let browser = exports.getBrowser(window);
if (!browser)
return null;
if (browser.id == "messagepane" && "currentHeaderData" in window && "content-base" in window.currentHeaderData)
{
// This is a blog entry
return window.currentHeaderData["content-base"].headerValue;
}
else if (browser.id == "messagepane" && "currentHeaderData" in window && "from" in window.currentHeaderData)
{
// This is a mail/newsgroup entry
try
{
let headerParser = Cc["@mozilla.org/messenger/headerparser;1"].getService(Ci.nsIMsgHeaderParser);
let emailAddress = headerParser.extractHeaderAddressMailboxes(window.currentHeaderData.from.headerValue);
return "mailto:" + emailAddress.replace(/^[\s"]+/, "").replace(/[\s"]+$/, "").replace(/\s/g, "%20");
}
catch(e)
{
return null;
}
}
else
return browser.currentURI;
}
exports.addBottomBar = function tb_addBottomBar(window, element)
{
let browser = exports.getBrowser(window);
if (!browser)
return;
let parent = window.document.getElementById("messagepanebox");
if (!parent || !(parent.compareDocumentPosition(browser) & Ci.nsIDOMNode.DOCUMENT_POSITION_CONTAINED_BY))
parent = browser.parentNode;
_addBottomBar(window, parent, element);
};
exports.removeBottomBar = _removeBottomBar;
let BrowserChangeListener = function(window, callback)
{
this.window = window;
this.callback = callback;
this.onSelect = this.onSelect.bind(this);
this.attach();
};
BrowserChangeListener.prototype = {
window: null,
callback: null,
currentBrowser: null,
setBrowser: function(browser)
{
if (browser != this.currentBrowser)
{
let oldBrowser = this.currentBrowser;
this.currentBrowser = browser;
this.callback(oldBrowser, browser);
}
},
onSelect: function()
{
this.setBrowser(exports.getBrowser(this.window));
},
attach: function()
{
this.onSelect();
let tabmail = this.window.document.getElementById("tabmail");
if (tabmail)
tabmail.tabContainer.addEventListener("select", this.onSelect, false);
},
detach: function()
{
let tabmail = this.window.document.getElementById("tabmail");
if (tabmail)
tabmail.tabContainer.removeEventListener("select", this.onSelect, false);
this.setBrowser(null);
}
};
exports.addBrowserLocationListener = function(/**Window*/ window, /**Function*/ callback, /**Boolean*/ ignoreSameDoc)
{
if (progressListeners.has(window))
{
progressListeners.get(window).locationCallbacks.push(callback);
return;
}
let callbacks = [callback];
let dummy = function() {};
let progressListener =
{
onLocationChange: function(progress, request, uri, flags)
{
if (!ignoreSameDoc || !flags || !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT))
for (let i = 0; i < callbacks.length; i++)
callbacks[i]();
},
onProgressChange: dummy,
onSecurityChange: dummy,
onStateChange: dummy,
onStatusChange: dummy,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference])
};
let messageListener =
{
onStartHeaders: dummy,
onEndHeaders: function()
{
let browser = exports.getBrowser(window);
if (browser.id == "messagepane")
for (let i = 0; i < callbacks.length; i++)
callbacks[i]();
},
onEndAttachments: dummy,
onBeforeShowHeaderPane: dummy
};
let listener = new BrowserChangeListener(window, function(oldBrowser, newBrowser)
{
if (oldBrowser)
oldBrowser.removeProgressListener(progressListener);
if (newBrowser)
newBrowser.addProgressListener(progressListener);
progressListener.onLocationChange();
});
listener.locationCallbacks = callbacks;
if ("gMessageListeners" in window)
window.gMessageListeners.push(messageListener);
listener.messageListener = messageListener;
progressListeners.set(window, listener);
};
exports.removeBrowserLocationListener = function(/**Window*/ window, /**Function*/ callback)
{
if (!progressListeners.has(window))
return;
let callbacks = progressListeners.get(window).locationCallbacks;
for (let i = 0; i < callbacks.length; i++)
if (callbacks[i] == callback)
callbacks.splice(i--, 1);
};
exports.removeBrowserLocationListeners = function(/**Window*/ window)
{
if (!progressListeners.has(window))
return;
let listener = progressListeners.get(window);
let messageListener = listener.messageListener;
let index = ("gMessageListeners" in window ? window.gMessageListeners.indexOf(messageListener) : -1);
if (index >= 0)
window.gMessageListeners.splice(index, 1);
listener.detach();
progressListeners.delete(window);
};
// Make sure to close/reopen list of blockable items when the user changes tabs
let {WindowObserver} = require("windowObserver");
new WindowObserver({
listeners: new WeakMap(),
applyToWindow: function(window)
{
if (!exports.isKnownWindow(window) || this.listeners.has(window))
return;
let {Utils} = require("utils");
Utils.runAsync(function()
{
let listener = new BrowserChangeListener(window, function(oldBrowser, newBrowser)
{
if (bottomBars.has(window))
{
let {UI} = require("ui")
UI.toggleBottombar(window);
UI.toggleBottombar(window);
}
});
this.listeners.set(window, listener);
}.bind(this));
},
removeFromWindow: function(window)
{
if (!this.listeners.has(window))
return;
let listener = this.listeners.get(window);
listener.detach();
this.listeners.delete(window);
}
});
break;
}
case "fennec2":
case "adblockbrowser":
{
exports.isKnownWindow = (window) => window.document.documentElement.id == "main-window";
exports.getBrowser = (window) => window.BrowserApp.selectedBrowser;
exports.addTab = (window, url, event) => window.BrowserApp.addTab(url, {selected: true});
let BrowserChangeListener = function(window, callback)
{
this.window = window;
this.callback = callback;
this.onSelect = this.onSelect.bind(this);
this.attach = this.attach.bind(this);
if (window.BrowserApp.deck)
this.attach();
else
window.addEventListener("UIReady", this.attach, false);
};
BrowserChangeListener.prototype = {
window: null,
callback: null,
currentBrowser: null,
setBrowser: function(browser)
{
if (browser != this.currentBrowser)
{
let oldBrowser = this.currentBrowser;
this.currentBrowser = browser;
this.callback(oldBrowser, browser);
}
},
onSelect: function()
{
let {Utils} = require("utils");
Utils.runAsync(function()
{
this.setBrowser(exports.getBrowser(this.window));
}.bind(this));
},
attach: function()
{
this.window.removeEventListener("UIReady", this.attach, false);
this.onSelect();
this.window.BrowserApp.deck.addEventListener("TabSelect", this.onSelect, false);
},
detach: function()
{
this.window.BrowserApp.deck.removeEventListener("TabSelect", this.onSelect, false);
this.setBrowser(null);
}
};
exports.addBrowserLocationListener = function ffn_addBrowserLocationListener(/**Window*/ window, /**Function*/ callback, /**Boolean*/ ignoreSameDoc)
{
if (progressListeners.has(window))
{
progressListeners.get(window).locationCallbacks.push(callback);
return;
}
let callbacks = [callback];
let dummy = function() {};
let progressListener =
{
onLocationChange: function(progress, request, uri, flags)
{
if (!ignoreSameDoc || !flags || !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT))
for (let i = 0; i < callbacks.length; i++)
callbacks[i]();
},
onProgressChange: dummy,
onSecurityChange: dummy,
onStateChange: dummy,
onStatusChange: dummy,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference])
};
let listener = new BrowserChangeListener(window, function(oldBrowser, newBrowser)
{
if (oldBrowser && typeof oldBrowser.removeProgressListener == "function")
oldBrowser.removeProgressListener(progressListener);
if (newBrowser && typeof newBrowser.removeProgressListener == "function")
newBrowser.addProgressListener(progressListener);
progressListener.onLocationChange();
});
listener.locationCallbacks = callbacks;
progressListeners.set(window, listener);
};
exports.removeBrowserLocationListener = function ffn_removeBrowserLocationListener(/**Window*/ window, /**Function*/ callback)
{
if (!progressListeners.has(window))
return;
let callbacks = progressListeners.get(window).locationCallbacks;
for (let i = 0; i < callbacks.length; i++)
if (callbacks[i] == callback)
callbacks.splice(i--, 1);
};
exports.removeBrowserLocationListeners = function ffn_removeBrowserLocationListeners(/**Window*/ window)
{
if (!progressListeners.has(window))
return;
let listener = progressListeners.get(window);
listener.detach();
progressListeners.delete(window);
};
let {Filter} = require("filterClasses");
let {Prefs} = require("prefs");
let {Policy} = require("contentPolicy");
let {UI} = require("ui");
let {Utils} = require("utils");
let toggleWhitelist = function(window)
{
if (!Prefs.enabled)
{
Prefs.enabled = true;
return;
}
let location = exports.getCurrentLocation(window);
let host = null;
if (location instanceof Ci.nsIURL && Policy.isBlockableScheme(location))
{
try
{
host = location.host.replace(/^www\./, "");
} catch (e) {}
}
if (!host)
return;
if (Policy.isWhitelisted(location.spec))
UI.removeWhitelist(window);
else
UI.toggleFilter(Filter.fromText("@@||" + host + "^$document"));
};
let menuItem = null;
onShutdown.add(function()
{
let window = null;
for (window of UI.applicationWindows)
break;
if (window && menuItem)
window.NativeWindow.menu.remove(menuItem);
});
UI.updateIconState = function fmn_updateIconState(window, icon)
{
if (menuItem !== null)
{
window.NativeWindow.menu.remove(menuItem);
menuItem = null;
}
let action;
let host = null;
if (Prefs.enabled)
{
let location = exports.getCurrentLocation(window);
if (location instanceof Ci.nsIURL && Policy.isBlockableScheme(location))
{
try
{
host = location.host.replace(/^www\./, "");
} catch (e) {}
}
if (!host)
return;
if (host && Policy.isWhitelisted(location.spec))
action = "enable_site";
else if (host)
action = "disable_site";
}
else
action = "enable";
let actionText = Utils.getString("mobile_menu_" + action);
if (host)
actionText = actionText.replace(/\?1\?/g, host);
let iconUrl = require("info").addonRoot + "icon64.png";
menuItem = window.NativeWindow.menu.add(actionText, iconUrl, toggleWhitelist.bind(null, window));
};
UI.openSubscriptionDialog = function(window, url, title, mainURL, mainTitle)
{
let dialogTitle = this.overlay.attributes.subscriptionDialogTitle;
let dialogMessage = this.overlay.attributes.subscriptionDialogMessage.replace(/\?1\?/, title).replace(/\?2\?/, url);
if (Utils.confirm(window, dialogMessage, dialogTitle))
this.setSubscription(url, title);
};
UI.openFiltersDialog = function()
{
let window = UI.currentWindow;
if (!window)
return
let browser = exports.addTab(window, "about:addons").browser;
browser.addEventListener("load", function openAddonPrefs(event)
{
browser.removeEventListener("load", openAddonPrefs, true);
Utils.runAsync(function()
{
// The page won't be ready until the add-on manager data is loaded so we call this method
// to know when the data will be ready.
AddonManager.getAddonsByTypes(["extension", "theme", "locale"], function()
{
let event = new Event("Event");
event.initEvent("popstate", true, false);
event.state = {id: require("info").addonID};
browser._contentWindow.dispatchEvent(event);
});
});
}, true);
};
break;
}
}