Revert "Move extensions to projects/"
This reverts commit 3e94e634ae54925da650a57b9982fe087658c580.
@@ -0,0 +1,32 @@
|
||||
#filter substitution
|
||||
|
||||
pref("extensions.@ADDON_CHROME_NAME@.currentVersion", "0.0");
|
||||
pref("extensions.@ADDON_CHROME_NAME@.enabled", true);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.frameobjects", true);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.fastcollapse", false);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.showinstatusbar", false);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.detachsidebar", false);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.defaulttoolbaraction", 0);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.defaultstatusbaraction", 0);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.sidebar_key", "Accel Shift V, Accel Shift U");
|
||||
pref("extensions.@ADDON_CHROME_NAME@.sendReport_key", "");
|
||||
pref("extensions.@ADDON_CHROME_NAME@.filters_key", "Accel Shift E, Accel Shift F, Accel Shift O");
|
||||
pref("extensions.@ADDON_CHROME_NAME@.enable_key", "");
|
||||
pref("extensions.@ADDON_CHROME_NAME@.flash_scrolltoitem", true);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.previewimages", true);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.data_directory", "adblockplus");
|
||||
pref("extensions.@ADDON_CHROME_NAME@.patternsbackups", 5);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.patternsbackupinterval", 24);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.whitelistschemes", "about chrome file irc moz-safe-about news resource snews x-jsd addbook cid imap mailbox nntp pop data javascript moz-icon");
|
||||
pref("extensions.@ADDON_CHROME_NAME@.hideimagemanager", true);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.subscriptions_autoupdate", true);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.subscriptions_listurl", "https://adblockplus.org/subscriptions2.xml");
|
||||
pref("extensions.@ADDON_CHROME_NAME@.subscriptions_fallbackurl", "https://adblockplus.org/getSubscription?version=%VERSION%&url=%SUBSCRIPTION%&downloadURL=%URL%&error=%ERROR%&channelStatus=%CHANNELSTATUS%&responseStatus=%RESPONSESTATUS%");
|
||||
pref("extensions.@ADDON_CHROME_NAME@.subscriptions_fallbackerrors", 5);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.documentation_link", "https://adblockplus.org/redirect?link=%LINK%&lang=%LANG%");
|
||||
pref("extensions.@ADDON_CHROME_NAME@.savestats", true);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.composer_default", 2);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.clearStatsOnHistoryPurge", true);
|
||||
pref("extensions.@ADDON_CHROME_NAME@.report_submiturl", "");
|
||||
pref("extensions.@ADDON_CHROME_NAME@.recentReports", "[]");
|
||||
pref("services.sync.engine.@ADDON_CHROME_NAME@", false);
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||
|
||||
<Description about="urn:mozilla:install-manifest">
|
||||
<em:id>@ADDON_ID@</em:id>
|
||||
<em:version>@ADDON_VERSION@</em:version>
|
||||
<em:name>@ADDON_NAME@</em:name>
|
||||
<em:description>@ADDON_SHORT_DESC@</em:description>
|
||||
<em:creator>@ADDON_AUTHOR@</em:creator>
|
||||
#ifdef ADDON_URL
|
||||
<em:homepageURL>@ADDON_URL@</em:homepageURL>
|
||||
#endif
|
||||
<em:type>2</em:type>
|
||||
<em:targetApplication>
|
||||
<Description>
|
||||
<!-- @ADDON_TARGET_APP_NAME@ -->
|
||||
<em:id>@ADDON_TARGET_APP_ID@</em:id>
|
||||
<em:minVersion>@ADDON_TARGET_APP_MINVER@</em:minVersion>
|
||||
<em:maxVersion>@ADDON_TARGET_APP_MAXVER@</em:maxVersion>
|
||||
</Description>
|
||||
</em:targetApplication>
|
||||
#ifdef ADDON_TARGET_BASILISK
|
||||
<em:targetApplication>
|
||||
<Description>
|
||||
<!-- Basilisk -->
|
||||
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
|
||||
<em:minVersion>52.0</em:minVersion>
|
||||
<em:maxVersion>55.*</em:maxVersion>
|
||||
<em:basilisk>true</em:basilisk>
|
||||
</Description>
|
||||
</em:targetApplication>
|
||||
#endif
|
||||
<em:targetApplication>
|
||||
<Description>
|
||||
<!-- Borealis (Navigator) -->
|
||||
<em:id>{a3210b97-8e8a-4737-9aa0-aa0e607640b9}</em:id>
|
||||
<em:minVersion>1.0.0a1</em:minVersion>
|
||||
<em:maxVersion>2.*</em:maxVersion>
|
||||
</Description>
|
||||
</em:targetApplication>
|
||||
<em:targetApplication>
|
||||
<Description>
|
||||
<!-- Interlink (Mail) -->
|
||||
<em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id>
|
||||
<em:minVersion>52.9.6884</em:minVersion>
|
||||
<em:maxVersion>52.9.*</em:maxVersion>
|
||||
</Description>
|
||||
</em:targetApplication>
|
||||
</Description>
|
||||
</RDF>
|
||||
@@ -0,0 +1,12 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#filter substitution
|
||||
|
||||
[.] chrome.jar:
|
||||
% resource @ADDON_CHROME_NAME@ /
|
||||
|
||||
% component {d32a3c00-4ed3-11de-8a39-0800200c9a66} components/Initializer.js
|
||||
% contract @adblockplus.org/abp/startup;1 {d32a3c00-4ed3-11de-8a39-0800200c9a66}
|
||||
% category profile-after-change @adblockplus.org/abp/startup;1 @adblockplus.org/abp/startup;1
|
||||
@@ -0,0 +1,16 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
FINAL_TARGET_FILES += [
|
||||
'icon.png',
|
||||
'icon64.png',
|
||||
]
|
||||
|
||||
FINAL_TARGET_PP_FILES += ['install.rdf']
|
||||
|
||||
FINAL_TARGET_PP_FILES.defaults += ['patterns.ini']
|
||||
FINAL_TARGET_PP_FILES.defaults.preferences += ['adblockplus.js']
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
@@ -0,0 +1,2 @@
|
||||
# Adblock Plus preferences
|
||||
version=4
|
||||
@@ -0,0 +1,8 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# Never add tier dirs after the application srcdir because they
|
||||
# apparently won't get packaged properly on Mac.
|
||||
DIRS += ['/abprime']
|
||||
@@ -0,0 +1,7 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package:
|
||||
cd $(DIST)/bin; \
|
||||
zip -Dr9X ../${ADDON_XPI_NAME}-${ADDON_VERSION}.xpi * -x \*/.mkdir.done; \
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
// Gecko 1.9.0/1.9.1 compatibility - add XPCOMUtils.defineLazyServiceGetter
|
||||
if (!("defineLazyServiceGetter" in XPCOMUtils))
|
||||
{
|
||||
XPCOMUtils.defineLazyServiceGetter = function XPCU_defineLazyServiceGetter(obj, prop, contract, iface)
|
||||
{
|
||||
obj.__defineGetter__(prop, function XPCU_serviceGetter()
|
||||
{
|
||||
delete obj[prop];
|
||||
return obj[prop] = Cc[contract].getService(Ci[iface]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Application startup/shutdown observer, triggers init()/shutdown() methods in Bootstrap.jsm module.
|
||||
* @constructor
|
||||
*/
|
||||
function Initializer() {}
|
||||
Initializer.prototype =
|
||||
{
|
||||
classDescription: "Adblock Plus initializer",
|
||||
contractID: "@adblockplus.org/abp/startup;1",
|
||||
classID: Components.ID("{d32a3c00-4ed3-11de-8a39-0800200c9a66}"),
|
||||
_xpcom_categories: [{ category: "app-startup", service: true }],
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
|
||||
|
||||
observe: function(subject, topic, data)
|
||||
{
|
||||
let observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
|
||||
switch (topic)
|
||||
{
|
||||
case "app-startup":
|
||||
observerService.addObserver(this, "profile-after-change", true);
|
||||
break;
|
||||
case "profile-after-change":
|
||||
observerService.addObserver(this, "quit-application", true);
|
||||
|
||||
// Don't init in Fennec, initialization will happen when UI is ready
|
||||
let appInfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
|
||||
if (appInfo.ID != "{a23983c0-fd0e-11dc-95ff-0800200c9a66}" && appInfo.ID != "{aa3c5121-dab2-40e2-81ca-7ea25febc110}")
|
||||
{
|
||||
try
|
||||
{
|
||||
// Gecko 2.0 and higher - chrome URLs can be loaded directly
|
||||
Cu.import("resource://@ADDON_CHROME_NAME@/modules/Bootstrap.jsm");
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Gecko 1.9.x - have to convert chrome URLs to file URLs first
|
||||
let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry);
|
||||
let ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
|
||||
let bootstrapURL = chromeRegistry.convertChromeURL(ioService.newURI("resource://@ADDON_CHROME_NAME@/modules/Bootstrap.jsm", null, null));
|
||||
Cu.import(bootstrapURL.spec);
|
||||
}
|
||||
Bootstrap.startup();
|
||||
}
|
||||
break;
|
||||
case "quit-application":
|
||||
try {
|
||||
// This will fail if component was added via chrome.manifest (Gecko 2.0)
|
||||
observerService.removeObserver(this, "profile-after-change");
|
||||
}catch(e) {}
|
||||
observerService.removeObserver(this, "quit-application");
|
||||
if ("@adblockplus.org/abp/private;1" in Cc)
|
||||
{
|
||||
let baseURL = Cc["@adblockplus.org/abp/private;1"].getService(Ci.nsIURI);
|
||||
Cu.import(baseURL.spec + "Bootstrap.jsm");
|
||||
Bootstrap.shutdown(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (XPCOMUtils.generateNSGetFactory)
|
||||
var NSGetFactory = XPCOMUtils.generateNSGetFactory([Initializer]);
|
||||
else
|
||||
var NSGetModule = XPCOMUtils.generateNSGetModule([Initializer]);
|
||||
@@ -0,0 +1,9 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# Don't use sub-manifest files
|
||||
NO_JS_MANIFEST = True
|
||||
|
||||
EXTRA_PP_COMPONENTS += ['Initializer.js']
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
try
|
||||
{
|
||||
Cu.import("resource://gre/modules/AddonManager.jsm");
|
||||
}
|
||||
catch (e) {}
|
||||
|
||||
function init()
|
||||
{
|
||||
let ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
|
||||
if (typeof AddonManager != "undefined")
|
||||
{
|
||||
let addon = AddonManager.getAddonByID(Utils.addonID, function(addon)
|
||||
{
|
||||
loadInstallManifest(addon.getResourceURI("install.rdf"), addon.name, addon.homepageURL);
|
||||
});
|
||||
}
|
||||
else if ("@mozilla.org/extensions/manager;1" in Cc)
|
||||
{
|
||||
let extensionManager = Cc["@mozilla.org/extensions/manager;1"].getService(Ci.nsIExtensionManager);
|
||||
let rdf = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService);
|
||||
let root = rdf.GetResource("urn:mozilla:item:" + Utils.addonID);
|
||||
|
||||
function emResource(prop)
|
||||
{
|
||||
return rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + prop);
|
||||
}
|
||||
|
||||
function getTarget(prop)
|
||||
{
|
||||
let target = extensionManager.datasource.GetTarget(root, emResource(prop), true);
|
||||
if (target)
|
||||
return target.QueryInterface(Ci.nsIRDFLiteral).Value;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
let installLocation = extensionManager.getInstallLocation(Utils.addonID);
|
||||
let installManifestFile = installLocation.getItemFile(Utils.addonID, "install.rdf");
|
||||
loadInstallManifest(ioService.newFileURI(installManifestFile), getTarget("name"), getTarget("homepageURL"));
|
||||
}
|
||||
else
|
||||
{
|
||||
// No add-on manager, no extension manager - we must be running in K-Meleon.
|
||||
// Load Manifest.jsm as last solution.
|
||||
Cu.import(baseURL + "Manifest.jsm");
|
||||
setExtensionData(manifest.name, manifest.version, manifest.homepage, [manifest.creator], manifest.contributors, manifest.translators);
|
||||
}
|
||||
}
|
||||
|
||||
function loadInstallManifest(installManifestURI, name, homepage)
|
||||
{
|
||||
let rdf = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService);
|
||||
let ds = rdf.GetDataSource(installManifestURI.spec);
|
||||
let root = rdf.GetResource("urn:mozilla:install-manifest");
|
||||
|
||||
function emResource(prop)
|
||||
{
|
||||
return rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + prop);
|
||||
}
|
||||
|
||||
function getTargets(prop)
|
||||
{
|
||||
let targets = ds.GetTargets(root, emResource(prop), true);
|
||||
let result = [];
|
||||
while (targets.hasMoreElements())
|
||||
result.push(targets.getNext().QueryInterface(Ci.nsIRDFLiteral).Value);
|
||||
return result;
|
||||
}
|
||||
|
||||
function dataSourceLoaded()
|
||||
{
|
||||
setExtensionData(name, getTargets("version")[0],
|
||||
homepage, getTargets("creator"),
|
||||
getTargets("contributor"), getTargets("translator"));
|
||||
}
|
||||
|
||||
if (ds instanceof Ci.nsIRDFRemoteDataSource && ds.loaded)
|
||||
dataSourceLoaded();
|
||||
else
|
||||
{
|
||||
let sink = ds.QueryInterface(Ci.nsIRDFXMLSink);
|
||||
sink.addXMLSinkObserver({
|
||||
onBeginLoad: function() {},
|
||||
onInterrupt: function() {},
|
||||
onResume: function() {},
|
||||
onEndLoad: function() {
|
||||
sink.removeXMLSinkObserver(this);
|
||||
dataSourceLoaded();
|
||||
},
|
||||
onError: function() {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function cmpNoCase(a, b)
|
||||
{
|
||||
let aLC = a.toLowerCase();
|
||||
let bLC = b.toLowerCase();
|
||||
if (aLC < bLC)
|
||||
return -1;
|
||||
else if (aLC > bLC)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
function setExtensionData(name, version, homepage, authors, contributors, translators)
|
||||
{
|
||||
authors.sort(cmpNoCase);
|
||||
contributors.sort(cmpNoCase);
|
||||
translators.sort(cmpNoCase);
|
||||
|
||||
E("title").value = name;
|
||||
E("version").value = version;
|
||||
E("homepage").value = homepage;
|
||||
E("authors").textContent = authors.join(", ");
|
||||
E("contributors").textContent = contributors.join(", ");
|
||||
E("translators").textContent = translators.join(", ");
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
request.open("GET", "chrome://@ADDON_CHROME_NAME@/content/subscriptions.xml");
|
||||
request.addEventListener("load", setSubscriptionAuthors, false);
|
||||
request.send(null);
|
||||
}
|
||||
|
||||
function setSubscriptionAuthors()
|
||||
{
|
||||
let doc = this.responseXML;
|
||||
if (!doc || doc.documentElement.localName != "subscriptions")
|
||||
return;
|
||||
|
||||
let authors = {__proto__: null};
|
||||
for (let node = doc.documentElement.firstChild; node; node = node.nextSibling)
|
||||
{
|
||||
if (node.localName != "subscription" || !node.hasAttribute("author"))
|
||||
continue;
|
||||
|
||||
for each (let author in node.getAttribute("author").split(","))
|
||||
{
|
||||
author = author.replace(/^\s+/, "").replace(/\s+$/, "");
|
||||
if (author == "")
|
||||
continue;
|
||||
|
||||
authors[author] = true;
|
||||
}
|
||||
}
|
||||
|
||||
let list = [];
|
||||
for (let author in authors)
|
||||
list.push(author);
|
||||
|
||||
list.sort(cmpNoCase)
|
||||
E("subscriptionAuthors").textContent = list.join(", ");
|
||||
|
||||
E("mainBox").setAttribute("loaded", "true");
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://@ADDON_CHROME_NAME@/skin/about.css" type="text/css"?>
|
||||
|
||||
<!DOCTYPE dialog SYSTEM "chrome://@ADDON_CHROME_NAME@/locale/about.dtd">
|
||||
|
||||
<dialog
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
title="&dialog.title;"
|
||||
id="adblockAboutWindow"
|
||||
windowtype="abp:about"
|
||||
onload="init()"
|
||||
buttons="accept">
|
||||
|
||||
<script type="application/x-javascript;version=1.7" src="utils.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="about.js"/>
|
||||
|
||||
<vbox id="mainBox">
|
||||
<description id="title" value=" "/>
|
||||
<hbox align="baseline">
|
||||
<label control="version" value="&version.title;"/>
|
||||
<textbox id="version" flex="1" class="plain" readonly="true" tabindex="-1"/>
|
||||
</hbox>
|
||||
|
||||
<groupbox id="mainGroup" flex="1">
|
||||
<description id="description">
|
||||
&description;
|
||||
</description>
|
||||
|
||||
<description id="homepageTitle" value="&homepage.label;"/>
|
||||
<description id="homepage" class="text-link" onclick="Utils.loadInBrowser(this.getAttribute('value'))"/>
|
||||
|
||||
<vbox id="authorsBox" align="top">
|
||||
<label id="authorsTitle" control="authors" value="&author.label;"/>
|
||||
<description id="authors"/>
|
||||
</vbox>
|
||||
|
||||
<vbox id="contributorsBox" align="top">
|
||||
<label id="contributorsTitle" control="contributors" value="&contributors.label;"/>
|
||||
<description id="contributors"/>
|
||||
</vbox>
|
||||
|
||||
<vbox id="subscriptionAuthorsBox" align="top">
|
||||
<label id="subscriptionAuthorsTitle" control="subscriptionAuthors" value="&subscriptionAuthors.label;"/>
|
||||
<description id="subscriptionAuthors"/>
|
||||
</vbox>
|
||||
|
||||
<vbox id="translatorsBox">
|
||||
<label id="translatorsTitle" control="translators" value="&translators.label;"/>
|
||||
<description id="translators"/>
|
||||
</vbox>
|
||||
</groupbox>
|
||||
</vbox>
|
||||
|
||||
</dialog>
|
||||
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
let nodes = null;
|
||||
let item = null;
|
||||
let advancedMode = false;
|
||||
|
||||
function init()
|
||||
{
|
||||
[nodes, item] = window.arguments;
|
||||
|
||||
E("filterType").value = (!item.filter || item.filter.disabled || item.filter instanceof WhitelistFilter ? "filterlist" : "whitelist");
|
||||
E("customPattern").value = item.location;
|
||||
|
||||
let insertionPoint = E("customPatternBox");
|
||||
let addSuggestion = function(address)
|
||||
{
|
||||
// Always drop protocol and www. from the suggestion
|
||||
address = address.replace(/^[\w\-]+:\/+(?:www\.)?/, "");
|
||||
|
||||
let suggestion = document.createElement("radio");
|
||||
suggestion.setAttribute("value", address);
|
||||
suggestion.setAttribute("label", address);
|
||||
suggestion.setAttribute("crop", "center");
|
||||
suggestion.setAttribute("class", "suggestion");
|
||||
insertionPoint.parentNode.insertBefore(suggestion, insertionPoint);
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
let ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
|
||||
try
|
||||
{
|
||||
let suggestions = [""];
|
||||
|
||||
let url = ioService.newURI(item.location, null, null)
|
||||
.QueryInterface(Ci.nsIURL);
|
||||
let suffix = (url.query ? "?*" : "");
|
||||
url.query = "";
|
||||
suggestions[1] = addSuggestion(url.spec + suffix);
|
||||
|
||||
let parentURL = ioService.newURI(url.fileName == "" ? ".." : ".", null, url);
|
||||
if (!parentURL.equals(url))
|
||||
suggestions[2] = addSuggestion(parentURL.spec + "*");
|
||||
else
|
||||
suggestions[2] = suggestions[1];
|
||||
|
||||
let rootURL = ioService.newURI("/", null, url);
|
||||
if (!rootURL.equals(parentURL) && !rootURL.equals(url))
|
||||
suggestions[3] = addSuggestion(rootURL.spec + "*");
|
||||
else
|
||||
suggestions[3] = suggestions[2];
|
||||
|
||||
try
|
||||
{
|
||||
suggestions[4] = addSuggestion(url.host.replace(/^www\./, "") + "^");
|
||||
|
||||
// Prefer example.com^ to example.com/*
|
||||
let undesired = suggestions[4].replace(/\^$/, "/*");
|
||||
for (let i = 0; i < suggestions.length - 1; i++)
|
||||
if (suggestions[i] == undesired)
|
||||
suggestions[i] = suggestions[4];
|
||||
|
||||
for (let child = insertionPoint.parentNode.firstChild; child; child = child.nextSibling)
|
||||
{
|
||||
if (child.localName == "radio" && child.getAttribute("value") == undesired)
|
||||
{
|
||||
child.parentNode.removeChild(child);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
suggestions[4] = suggestions[3];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
let effectiveTLD = Cc["@mozilla.org/network/effective-tld-service;1"].getService(Ci.nsIEffectiveTLDService);
|
||||
let host = url.host;
|
||||
let baseDomain = effectiveTLD.getBaseDomainFromHost(host);
|
||||
if (baseDomain != host.replace(/^www\./, ""))
|
||||
suggestions[5] = addSuggestion(baseDomain + "^");
|
||||
else
|
||||
suggestions[5] = suggestions[4];
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
suggestions[5] = suggestions[4];
|
||||
}
|
||||
|
||||
E("patternGroup").value = (Prefs.composer_default in suggestions ? suggestions[Prefs.composer_default] : suggestions[1]);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// IOService returned nsIURI - not much we can do with it
|
||||
addSuggestion(item.location);
|
||||
E("patternGroup").value = "";
|
||||
}
|
||||
if (Prefs.composer_default == 0)
|
||||
E("customPattern").focus();
|
||||
else
|
||||
E("patternGroup").focus();
|
||||
|
||||
let types = [];
|
||||
for (let type in Policy.localizedDescr)
|
||||
{
|
||||
types.push(parseInt(type));
|
||||
}
|
||||
types.sort(function(a, b) {
|
||||
if (a < b)
|
||||
return -1;
|
||||
else if (a > b)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
});
|
||||
|
||||
let docDomain = item.docDomain;
|
||||
let thirdParty = item.thirdParty;
|
||||
|
||||
if (docDomain)
|
||||
docDomain = docDomain.replace(/^www\./i, "").replace(/\.+$/, "");
|
||||
if (docDomain)
|
||||
E("domainRestriction").value = docDomain;
|
||||
|
||||
E("thirdParty").hidden = !thirdParty;
|
||||
E("firstParty").hidden = thirdParty;
|
||||
|
||||
let typeGroup = E("typeGroup");
|
||||
let defaultTypes = RegExpFilter.prototype.contentType & ~RegExpFilter.typeMap.DOCUMENT;
|
||||
let isDefaultType = (RegExpFilter.typeMap[item.typeDescr] & defaultTypes) != 0;
|
||||
for each (let type in types)
|
||||
{
|
||||
if (type == Policy.type.ELEMHIDE)
|
||||
continue;
|
||||
|
||||
let typeNode = document.createElement("checkbox");
|
||||
typeNode.setAttribute("value", Policy.typeDescr[type].toLowerCase().replace(/\_/g, "-"));
|
||||
typeNode.setAttribute("label", Policy.localizedDescr[type].toLowerCase());
|
||||
|
||||
let typeMask = RegExpFilter.typeMap[Policy.typeDescr[type]];
|
||||
typeNode._defaultType = (typeMask & defaultTypes) != 0;
|
||||
if ((isDefaultType && typeNode._defaultType) || (!isDefaultType && item.type == type))
|
||||
typeNode.setAttribute("checked", "true");
|
||||
|
||||
if (item.type == type)
|
||||
typeNode.setAttribute("disabled", "true");
|
||||
typeNode.addEventListener("command", function() checkboxUpdated(this), false);
|
||||
typeGroup.appendChild(typeNode);
|
||||
}
|
||||
|
||||
let collapseDefault = E("collapseDefault");
|
||||
collapseDefault.label = collapseDefault.getAttribute(Prefs.fastcollapse ? "label_no" : "label_yes");
|
||||
E("collapse").value = "";
|
||||
E("collapse").setAttribute("label", collapseDefault.label);
|
||||
|
||||
let warning = E("disabledWarning");
|
||||
generateLinkText(warning);
|
||||
warning.hidden = Prefs.enabled;
|
||||
|
||||
updatePatternSelection();
|
||||
}
|
||||
|
||||
function checkboxUpdated(checkbox)
|
||||
{
|
||||
checkbox._lastChange = Date.now();
|
||||
updateFilter();
|
||||
}
|
||||
|
||||
function updateFilter()
|
||||
{
|
||||
let filter = "";
|
||||
|
||||
let type = E("filterType").value
|
||||
if (type == "whitelist")
|
||||
filter += "@@";
|
||||
|
||||
let pattern = E("patternGroup").value;
|
||||
if (pattern == "")
|
||||
pattern = E("customPattern").value;
|
||||
|
||||
if (E("anchorStart").checked)
|
||||
filter += E("anchorStart").flexibleAnchor ? "||" : "|";
|
||||
|
||||
filter += pattern;
|
||||
|
||||
if (E("anchorEnd").checked)
|
||||
filter += "|";
|
||||
|
||||
if (advancedMode)
|
||||
{
|
||||
let options = [];
|
||||
|
||||
if (E("domainRestrictionEnabled").checked)
|
||||
{
|
||||
let domainRestriction = E("domainRestriction").value.replace(/[,\s]/g, "").replace(/\.+$/, "");
|
||||
if (domainRestriction)
|
||||
options.push([E("domainRestrictionEnabled")._lastChange || 0, "domain=" + domainRestriction]);
|
||||
}
|
||||
|
||||
if (E("firstParty").checked)
|
||||
options.push([E("firstParty")._lastChange || 0, "~third-party"]);
|
||||
if (E("thirdParty").checked)
|
||||
options.push([E("thirdParty")._lastChange || 0, "third-party"]);
|
||||
|
||||
if (E("matchCase").checked)
|
||||
options.push([E("matchCase")._lastChange || 0, "match-case"]);
|
||||
|
||||
let collapse = E("collapse");
|
||||
disableElement(collapse, type == "whitelist", "value", "");
|
||||
if (collapse.value != "")
|
||||
options.push([collapse._lastChange, collapse.value]);
|
||||
|
||||
let enabledTypes = [];
|
||||
let disabledTypes = [];
|
||||
let forceEnabledTypes = [];
|
||||
for (let typeNode = E("typeGroup").firstChild; typeNode; typeNode = typeNode.nextSibling)
|
||||
{
|
||||
let value = typeNode.getAttribute("value");
|
||||
if (value == "document")
|
||||
disableElement(typeNode, type != "whitelist", "checked", false);
|
||||
|
||||
if (!typeNode._defaultType)
|
||||
{
|
||||
if (typeNode.getAttribute("checked") == "true")
|
||||
forceEnabledTypes.push([typeNode._lastChange || 0, value]);
|
||||
}
|
||||
else if (typeNode.getAttribute("checked") == "true")
|
||||
enabledTypes.push([typeNode._lastChange || 0, value]);
|
||||
else
|
||||
disabledTypes.push([typeNode._lastChange || 0, "~" + value]);
|
||||
}
|
||||
if (!forceEnabledTypes.length && disabledTypes.length < enabledTypes.length)
|
||||
options.push.apply(options, disabledTypes);
|
||||
else
|
||||
options.push.apply(options, enabledTypes);
|
||||
options.push.apply(options, forceEnabledTypes);
|
||||
|
||||
if (options.length)
|
||||
{
|
||||
options.sort(function(a, b) a[0] - b[0]);
|
||||
filter += "$" + options.map(function(o) o[1]).join(",");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
let defaultTypes = RegExpFilter.prototype.contentType & ~RegExpFilter.typeMap.DOCUMENT;
|
||||
let isDefaultType = (RegExpFilter.typeMap[item.typeDescr] & defaultTypes) != 0;
|
||||
if (!isDefaultType)
|
||||
filter += "$" + item.typeDescr.toLowerCase().replace(/\_/g, "-");
|
||||
}
|
||||
|
||||
filter = Filter.normalize(filter);
|
||||
E("regexpWarning").hidden = !Filter.regexpRegExp.test(filter);
|
||||
|
||||
let isSlow = false;
|
||||
let compiledFilter = Filter.fromText(filter);
|
||||
if (E("regexpWarning").hidden)
|
||||
{
|
||||
if (compiledFilter instanceof RegExpFilter && defaultMatcher.isSlowFilter(compiledFilter))
|
||||
isSlow = true;
|
||||
}
|
||||
E("shortpatternWarning").hidden = !isSlow;
|
||||
|
||||
E("matchWarning").hidden = compiledFilter instanceof RegExpFilter && compiledFilter.matches(item.location, item.typeDescr, item.docDomain, item.thirdParty);
|
||||
|
||||
E("filter").value = filter;
|
||||
}
|
||||
|
||||
function generateLinkText(element, replacement)
|
||||
{
|
||||
let template = element.getAttribute("textTemplate");
|
||||
if (typeof replacement != "undefined")
|
||||
template = template.replace(/\?1\?/g, replacement)
|
||||
|
||||
let [, beforeLink, linkText, afterLink] = /(.*)\[link\](.*)\[\/link\](.*)/.exec(template) || [null, "", template, ""];
|
||||
while (element.firstChild && element.firstChild.nodeType != Node.ELEMENT_NODE)
|
||||
element.removeChild(element.firstChild);
|
||||
while (element.lastChild && element.lastChild.nodeType != Node.ELEMENT_NODE)
|
||||
element.removeChild(element.lastChild);
|
||||
if (!element.firstChild)
|
||||
return;
|
||||
|
||||
element.firstChild.textContent = linkText;
|
||||
element.insertBefore(document.createTextNode(beforeLink), element.firstChild);
|
||||
element.appendChild(document.createTextNode(afterLink));
|
||||
}
|
||||
|
||||
function updatePatternSelection()
|
||||
{
|
||||
let pattern = E("patternGroup").value;
|
||||
if (pattern == "")
|
||||
{
|
||||
pattern = E("customPattern").value;
|
||||
}
|
||||
else
|
||||
{
|
||||
E("anchorStart").checked = true;
|
||||
E("anchorEnd").checked = false;
|
||||
}
|
||||
|
||||
function testFilter(/**String*/ filter) /**Boolean*/
|
||||
{
|
||||
return RegExpFilter.fromText(filter + "$" + item.typeDescr).matches(item.location, item.typeDescr, item.docDomain, item.thirdParty);
|
||||
}
|
||||
|
||||
let anchorStartCheckbox = E("anchorStart");
|
||||
if (!/^\*/.test(pattern) && testFilter("||" + pattern))
|
||||
{
|
||||
disableElement(anchorStartCheckbox, false, "checked", false);
|
||||
anchorStartCheckbox.setAttribute("label", anchorStartCheckbox.getAttribute("labelFlexible"));
|
||||
anchorStartCheckbox.accessKey = anchorStartCheckbox.getAttribute("accesskeyFlexible");
|
||||
anchorStartCheckbox.flexibleAnchor = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
disableElement(anchorStartCheckbox, /^\*/.test(pattern) || !testFilter("|" + pattern), "checked", false);
|
||||
anchorStartCheckbox.setAttribute("label", anchorStartCheckbox.getAttribute("labelRegular"));
|
||||
anchorStartCheckbox.accessKey = anchorStartCheckbox.getAttribute("accesskeyRegular");
|
||||
anchorStartCheckbox.flexibleAnchor = false;
|
||||
}
|
||||
disableElement(E("anchorEnd"), /[\*\^]$/.test(pattern) || !testFilter(pattern + "|"), "checked", false);
|
||||
|
||||
updateFilter();
|
||||
setAdvancedMode(document.documentElement.getAttribute("advancedMode") == "true");
|
||||
}
|
||||
|
||||
function updateCustomPattern()
|
||||
{
|
||||
E("patternGroup").value = "";
|
||||
updatePatternSelection();
|
||||
}
|
||||
|
||||
function addFilter() {
|
||||
let filter = Filter.fromText(document.getElementById("filter").value);
|
||||
filter.disabled = false;
|
||||
|
||||
FilterStorage.addFilter(filter);
|
||||
|
||||
if (nodes)
|
||||
Policy.refilterNodes(nodes, item);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function setAdvancedMode(mode) {
|
||||
advancedMode = mode;
|
||||
|
||||
var dialog = document.documentElement;
|
||||
dialog.setAttribute("advancedMode", advancedMode);
|
||||
|
||||
var button = dialog.getButton("disclosure");
|
||||
button.setAttribute("label", dialog.getAttribute(advancedMode ? "buttonlabeldisclosure_off" : "buttonlabeldisclosure_on"));
|
||||
|
||||
updateFilter();
|
||||
}
|
||||
|
||||
function disableElement(element, disable, valueProperty, disabledValue) {
|
||||
if ((element.getAttribute("disabled") == "true") == disable)
|
||||
return;
|
||||
|
||||
if (disable)
|
||||
{
|
||||
element.setAttribute("disabled", "true");
|
||||
element._abpStoredValue = element[valueProperty];
|
||||
element[valueProperty] = disabledValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
element.removeAttribute("disabled");
|
||||
if ("_abpStoredValue" in element)
|
||||
element[valueProperty] = element._abpStoredValue;
|
||||
delete element._abpStoredValue;
|
||||
}
|
||||
}
|
||||
|
||||
function openPreferences() {
|
||||
Utils.openFiltersDialog(Filter.fromText(E("filter").value));
|
||||
}
|
||||
|
||||
function doEnable() {
|
||||
Prefs.enabled = true;
|
||||
E("disabledWarning").hidden = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects or unselects all type checkboxes except those
|
||||
* that are disabled.
|
||||
*/
|
||||
function selectAllTypes(/**Boolean*/ select)
|
||||
{
|
||||
for (let typeNode = E("typeGroup").firstChild; typeNode; typeNode = typeNode.nextSibling)
|
||||
if (typeNode.getAttribute("disabled") != "true")
|
||||
typeNode.checked = select;
|
||||
updateFilter();
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<!DOCTYPE overlay SYSTEM "chrome://@ADDON_CHROME_NAME@/locale/composer.dtd">
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://@ADDON_CHROME_NAME@/skin/composer.css" type="text/css"?>
|
||||
|
||||
<dialog id="abp-composer"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
title="&dialog.title;"
|
||||
onload="init()"
|
||||
ondialogaccept="return addFilter()"
|
||||
ondialogdisclosure="setAdvancedMode(!advancedMode)"
|
||||
buttons="accept,cancel,disclosure"
|
||||
width="800px"
|
||||
height="400px"
|
||||
persist="screenX screenY width height sizemode advancedMode"
|
||||
advancedMode="false"
|
||||
buttonlabelaccept="&accept.label;"
|
||||
buttonlabeldisclosure="&advanced.label;"
|
||||
buttonlabeldisclosure_on="&advanced.label;"
|
||||
buttonlabeldisclosure_off="&basic.label;"
|
||||
windowtype="abp:composer">
|
||||
|
||||
<script type="application/x-javascript;version=1.7" src="utils.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="composer.js"/>
|
||||
|
||||
<popupset>
|
||||
<tooltip id="domainRestrictionHelp" label="&domainRestriction.help;"/>
|
||||
</popupset>
|
||||
|
||||
<description id="disabledWarning" hidden="true" textTemplate="&disabled.warning;">
|
||||
<label class="text-link" onclick="doEnable()"/>
|
||||
</description>
|
||||
|
||||
<hbox id="filterBox" align="center">
|
||||
<label control="filter" value="&filter.label;" accesskey="&filter.accesskey;"/>
|
||||
<textbox id="filter" flex="1" tabindex="-1" readonly="true"/>
|
||||
<button id="preferences" label="&preferences.label;" accesskey="&preferences.accesskey;" oncommand="openPreferences()"/>
|
||||
</hbox>
|
||||
|
||||
<radiogroup orient="horizontal" id="filterType" oncommand="updateFilter()">
|
||||
<radio label="&type.filter.label;" accesskey="&type.filter.accesskey;" value="filterlist" flex="1"/>
|
||||
<radio label="&type.whitelist.label;" accesskey="&type.whitelist.accesskey;" value="whitelist" flex="1"/>
|
||||
</radiogroup>
|
||||
|
||||
<hbox flex="1">
|
||||
<groupbox id="pattern" flex="1">
|
||||
<caption label="&pattern.label;"/>
|
||||
<radiogroup id="patternGroup" flex="1" oncommand="updatePatternSelection()" style="overflow: auto;">
|
||||
<description id="patternExplanation">&pattern.explanation;</description>
|
||||
<description id="regexpWarning" hidden="true">®exp.warning;</description>
|
||||
<description id="shortpatternWarning" hidden="true">&shortpattern.warning;</description>
|
||||
<description id="matchWarning" hidden="true">&match.warning;</description>
|
||||
<hbox id="customPatternBox">
|
||||
<radio id="customPatternRadio" label="&custom.pattern.label;" accesskey="&custom.pattern.accesskey;" value="" control="customPattern"/>
|
||||
<textbox id="customPattern" flex="1" oninput="updateCustomPattern()"/>
|
||||
</hbox>
|
||||
</radiogroup>
|
||||
<hbox id="anchorGroup" pack="start" align="baseline">
|
||||
<label value="&anchors.label;"/>
|
||||
<description flex="1" style="margin: 0; padding: 0;">
|
||||
<checkbox id="anchorStart" labelRegular="&anchor.start.label;" accesskeyRegular="&anchor.start.accesskey;"
|
||||
labelFlexible="&anchor.start.flexible.label;" accesskeyFlexible="&anchor.start.flexible.accesskey;"
|
||||
oncommand="updateFilter()"/>
|
||||
<checkbox id="anchorEnd" label="&anchor.end.label;" accesskey="&anchor.end.accesskey;" oncommand="updateFilter()"/>
|
||||
</description>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
<groupbox id="options">
|
||||
<caption label="&options.label;"/>
|
||||
<checkbox id="firstParty" label="&firstParty.label;" accesskey="&firstParty.accesskey;" oncommand="checkboxUpdated(this);"/>
|
||||
<checkbox id="thirdParty" label="&thirdParty.label;" accesskey="&thirdParty.accesskey;" oncommand="checkboxUpdated(this);"/>
|
||||
<checkbox id="matchCase" label="&matchCase.label;" accesskey="&matchCase.accesskey;" oncommand="checkboxUpdated(this);"/>
|
||||
<hbox align="baseline">
|
||||
<checkbox id="domainRestrictionEnabled" label="&domainRestriction.label;" accesskey="&domainRestriction.accesskey;" oncommand="checkboxUpdated(this);"/>
|
||||
<description class="help" value="?" tooltip="domainRestrictionHelp"/>
|
||||
</hbox>
|
||||
<textbox id="domainRestriction" oninput="updateFilter()"/>
|
||||
|
||||
<label id="typeGroupLabel" value="&types.label;"/>
|
||||
<hbox>
|
||||
<label id="selectAllTypes" class="text-link" value="&selectAllTypes.label;" onclick="selectAllTypes(true)"/>
|
||||
<spacer flex="1"/>
|
||||
<label id="unselectAllTypes" class="text-link" value="&unselectAllTypes.label;" onclick="selectAllTypes(false)"/>
|
||||
</hbox>
|
||||
<vbox flex="1" id="typeGroup"/>
|
||||
|
||||
<vbox>
|
||||
<label control="collapse" value="&collapse.label;" accesskey="&collapse.accesskey;"/>
|
||||
<menulist id="collapse" oncommand="updateFilter()">
|
||||
<menupopup>
|
||||
<menuitem id="collapseDefault" value="" label_yes="&collapse.default.yes.label;" label_no="&collapse.default.no.label;" selected="true"/>
|
||||
<menuitem label="&collapse.yes.label;" value="collapse"/>
|
||||
<menuitem label="&collapse.no.label;" value="~collapse"/>
|
||||
</menupopup>
|
||||
</menulist>
|
||||
</vbox>
|
||||
</groupbox>
|
||||
</hbox>
|
||||
</dialog>
|
||||
@@ -0,0 +1,96 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Adblock Plus Errors</title>
|
||||
<style type="text/css">
|
||||
.warning, .error
|
||||
{
|
||||
border: 1px dashed black;
|
||||
margin: 5px;
|
||||
padding: 2px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.error
|
||||
{
|
||||
background-color: #fff0f0;
|
||||
}
|
||||
|
||||
.warning
|
||||
{
|
||||
background-color: #ffffe0;
|
||||
}
|
||||
|
||||
button
|
||||
{
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button onclick="window.location.reload();">Refresh</button>
|
||||
<button onclick="clearErrors();">Clear errors</button>
|
||||
|
||||
<script type="application/x-javascript;version=1.7">
|
||||
try {
|
||||
Components.utils.import("chrome://adblockplus-modules/content/Utils.jsm");
|
||||
|
||||
let text = "You are running Adblock Plus " + Utils.addonVersion;
|
||||
let build = Utils.addonBuild;
|
||||
if (build)
|
||||
text += ", build " + build;
|
||||
text += ".";
|
||||
document.write("<p>" + text + "</p>");
|
||||
} catch (e) {}
|
||||
|
||||
let messages = {};
|
||||
Components.classes["@mozilla.org/consoleservice;1"]
|
||||
.getService(Components.interfaces.nsIConsoleService)
|
||||
.getMessageArray(messages, {});
|
||||
messages = messages.value || [];
|
||||
|
||||
messages = messages.filter(function(message)
|
||||
{
|
||||
return (message instanceof Components.interfaces.nsIScriptError &&
|
||||
!/^https?:/i.test(message.sourceName) &&
|
||||
(/adblock/i.test(message.errorMessage) || /adblock/i.test(message.sourceName)));
|
||||
});
|
||||
|
||||
if (messages.length)
|
||||
{
|
||||
document.write("<p>Errors related to Adblock Plus:</p>");
|
||||
|
||||
for each (let message in messages)
|
||||
{
|
||||
let type = (message.flags & Components.interfaces.nsIScriptError.warningFlag ? "warning" : "error");
|
||||
let html = "<b>" + (type == "warning" ? "Warning:" : "Error:") + "</b><br>";
|
||||
html += encodeHTML(message.errorMessage) + "<br><br>";
|
||||
if (message.sourceLine)
|
||||
html += "Source line: " + encodeHTML(message.sourceLine) + "<br>";
|
||||
if (message.sourceName)
|
||||
html += "Location: " + encodeHTML(message.sourceName) + " line " + message.lineNumber + "<br>";
|
||||
html = html.replace(/(<br>)+$/, "");
|
||||
document.write("<div class='" + type + "'>" +
|
||||
html +
|
||||
"</div>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
document.write("<p>No errors found.</p>");
|
||||
}
|
||||
|
||||
function encodeHTML(string)
|
||||
{
|
||||
return string.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function clearErrors()
|
||||
{
|
||||
Components.classes["@mozilla.org/consoleservice;1"]
|
||||
.getService(Components.interfaces.nsIConsoleService)
|
||||
.reset();
|
||||
window.location.reload();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
|
||||
/**
|
||||
* Implementation of backup and restore functionality.
|
||||
* @class
|
||||
*/
|
||||
var Backup =
|
||||
{
|
||||
/**
|
||||
* Template for menu items to be displayed in the Restore menu (for automated
|
||||
* backups).
|
||||
* @type Element
|
||||
*/
|
||||
restoreTemplate: null,
|
||||
|
||||
/**
|
||||
* Element after which restore items should be inserted.
|
||||
* @type Element
|
||||
*/
|
||||
restoreInsertionPoint: null,
|
||||
|
||||
/**
|
||||
* Regular expression to recognize checksum comments.
|
||||
*/
|
||||
CHECKSUM_REGEXP: /^!\s*checksum[\s\-:]+([\w\+\/]+)/i,
|
||||
|
||||
/**
|
||||
* Regular expression to recognize group title comments.
|
||||
*/
|
||||
GROUPTITLE_REGEXP: /^!\s*\[(.*)\]((?:\/\w+)*)\s*$/,
|
||||
|
||||
|
||||
/**
|
||||
* Initializes backup UI.
|
||||
*/
|
||||
init: function()
|
||||
{
|
||||
this.restoreTemplate = E("restoreBackupTemplate");
|
||||
this.restoreInsertionPoint = this.restoreTemplate.previousSibling;
|
||||
this.restoreTemplate.parentNode.removeChild(this.restoreTemplate);
|
||||
this.restoreTemplate.removeAttribute("id");
|
||||
this.restoreTemplate.removeAttribute("hidden");
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the default download dir, as used by the browser itself.
|
||||
*/
|
||||
getDefaultDir: function() /**nsIFile*/
|
||||
{
|
||||
try
|
||||
{
|
||||
return Utils.prefService.getComplexValue("browser.download.lastDir", Ci.nsILocalFile);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// No default download location. Default to desktop.
|
||||
return FileUtils.getDir("Desk", [], false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves new default download dir after the user chose a different directory to
|
||||
* save his files to.
|
||||
*/
|
||||
saveDefaultDir: function(/**nsIFile*/ dir)
|
||||
{
|
||||
try
|
||||
{
|
||||
Utils.prefService.setComplexValue("browser.download.lastDir", Ci.nsILocalFile, dir);
|
||||
} catch(e) {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the Restore menu is being opened, fills in "Automated backup"
|
||||
* entries.
|
||||
*/
|
||||
fillRestorePopup: function()
|
||||
{
|
||||
while (this.restoreInsertionPoint.nextSibling && !this.restoreInsertionPoint.nextSibling.id)
|
||||
this.restoreInsertionPoint.parentNode.removeChild(this.restoreInsertionPoint.nextSibling);
|
||||
|
||||
let files = FilterStorage.getBackupFiles().reverse();
|
||||
for (let i = 0; i < files.length; i++)
|
||||
{
|
||||
let file = files[i];
|
||||
let item = this.restoreTemplate.cloneNode(true);
|
||||
let label = item.getAttribute("label");
|
||||
label = label.replace(/\?1\?/, Utils.formatTime(file.lastModifiedTime));
|
||||
item.setAttribute("label", label);
|
||||
item.addEventListener("command", function()
|
||||
{
|
||||
Backup.restoreAllData(file);
|
||||
}, false);
|
||||
this.restoreInsertionPoint.parentNode.insertBefore(item, this.restoreInsertionPoint.nextSibling);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Lets the user choose a file to restore filters from.
|
||||
*/
|
||||
restoreFromFile: function()
|
||||
{
|
||||
let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
||||
picker.init(window, E("backupButton").getAttribute("_restoreDialogTitle"), picker.modeOpen);
|
||||
picker.defaultExtension = ".ini";
|
||||
picker.appendFilter(E("backupButton").getAttribute("_fileFilterComplete"), "*.ini");
|
||||
picker.appendFilter(E("backupButton").getAttribute("_fileFilterCustom"), "*.txt");
|
||||
|
||||
if (picker.show() != picker.returnCancel)
|
||||
{
|
||||
this.saveDefaultDir(picker.file.parent);
|
||||
if (picker.filterIndex == 0)
|
||||
this.restoreAllData(picker.file);
|
||||
else
|
||||
this.restoreCustomFilters(picker.file);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores patterns.ini from a file.
|
||||
*/
|
||||
restoreAllData: function(/**nsIFile*/ file)
|
||||
{
|
||||
let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
|
||||
stream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
|
||||
stream.QueryInterface(Ci.nsILineInputStream);
|
||||
|
||||
let lines = [];
|
||||
let line = {value: null};
|
||||
if (stream.readLine(line))
|
||||
lines.push(line.value);
|
||||
if (stream.readLine(line))
|
||||
lines.push(line.value);
|
||||
stream.close();
|
||||
|
||||
let match;
|
||||
if (lines.length < 2 || lines[0] != "# Adblock Plus preferences" || !(match = /version=(\d+)/.exec(lines[1])))
|
||||
{
|
||||
Utils.alert(window, E("backupButton").getAttribute("_restoreError"), E("backupButton").getAttribute("_restoreDialogTitle"));
|
||||
return;
|
||||
}
|
||||
|
||||
let warning = E("backupButton").getAttribute("_restoreCompleteWarning");
|
||||
let minVersion = parseInt(match[1], 10);
|
||||
if (minVersion > FilterStorage.formatVersion)
|
||||
warning += "\n\n" + E("backupButton").getAttribute("_restoreVersionWarning");
|
||||
|
||||
if (!Utils.confirm(window, warning, E("backupButton").getAttribute("_restoreDialogTitle")))
|
||||
return;
|
||||
|
||||
FilterStorage.loadFromDisk(file);
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores custom filters from a file.
|
||||
*/
|
||||
restoreCustomFilters: function(/**nsIFile*/ file)
|
||||
{
|
||||
IO.readFromFile(file, true, {
|
||||
seenHeader: false,
|
||||
subscription: null,
|
||||
process: function(line)
|
||||
{
|
||||
if (!this.seenHeader)
|
||||
{
|
||||
// This should be a header
|
||||
this.seenHeader = true;
|
||||
let match = /\[Adblock(?:\s*Plus\s*([\d\.]+)?)?\]/i.exec(line);
|
||||
if (match)
|
||||
{
|
||||
let warning = E("backupButton").getAttribute("_restoreCustomWarning");
|
||||
let minVersion = match[1];
|
||||
if (minVersion && Utils.versionComparator.compare(minVersion, Utils.addonVersion) > 0)
|
||||
warning += "\n\n" + E("backupButton").getAttribute("_restoreVersionWarning");
|
||||
|
||||
if (Utils.confirm(window, warning, E("backupButton").getAttribute("_restoreDialogTitle")))
|
||||
{
|
||||
let subscriptions = FilterStorage.subscriptions.filter(function(s) s instanceof SpecialSubscription);
|
||||
for (let i = 0; i < subscriptions.length; i++)
|
||||
FilterStorage.removeSubscription(subscriptions[i]);
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
throw Cr.NS_BASE_STREAM_WOULD_BLOCK;
|
||||
}
|
||||
else
|
||||
throw new Error("Invalid file");
|
||||
}
|
||||
else if (line === null)
|
||||
{
|
||||
// End of file
|
||||
if (this.subscription)
|
||||
FilterStorage.addSubscription(this.subscription);
|
||||
E("tabs").selectedIndex = 1;
|
||||
}
|
||||
else if (Backup.CHECKSUM_REGEXP.test(line))
|
||||
{
|
||||
// Ignore checksums
|
||||
}
|
||||
else if (Backup.GROUPTITLE_REGEXP.test(line))
|
||||
{
|
||||
// New group start
|
||||
if (this.subscription)
|
||||
FilterStorage.addSubscription(this.subscription);
|
||||
|
||||
let [, title, options] = Backup.GROUPTITLE_REGEXP.exec(line);
|
||||
this.subscription = SpecialSubscription.create(title);
|
||||
|
||||
let defaults = [];
|
||||
if (options)
|
||||
options = options.split("/");
|
||||
for (let j = 0; j < options.length; j++)
|
||||
if (options[j] in SpecialSubscription.defaultsMap)
|
||||
defaults.push(options[j]);
|
||||
if (defaults.length)
|
||||
this.subscription.defaults = defaults;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular filter
|
||||
let filter = Filter.fromText(Filter.normalize(line));
|
||||
if (filter)
|
||||
{
|
||||
if (!this.subscription)
|
||||
this.subscription = SpecialSubscription.create(Utils.getString("newGroup_title"));
|
||||
this.subscription.filters.push(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, function(e)
|
||||
{
|
||||
if (e && e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
Utils.alert(window, E("backupButton").getAttribute("_restoreError"), E("backupButton").getAttribute("_restoreDialogTitle"));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Lets the user choose a file to backup filters to.
|
||||
*/
|
||||
backupToFile: function()
|
||||
{
|
||||
let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
||||
picker.init(window, E("backupButton").getAttribute("_backupDialogTitle"), picker.modeSave);
|
||||
picker.defaultExtension = ".ini";
|
||||
picker.appendFilter(E("backupButton").getAttribute("_fileFilterComplete"), "*.ini");
|
||||
picker.appendFilter(E("backupButton").getAttribute("_fileFilterCustom"), "*.txt");
|
||||
|
||||
if (picker.show() != picker.returnCancel)
|
||||
{
|
||||
this.saveDefaultDir(picker.file.parent);
|
||||
if (picker.filterIndex == 0)
|
||||
this.backupAllData(picker.file);
|
||||
else
|
||||
this.backupCustomFilters(picker.file);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes all patterns.ini data to a file.
|
||||
*/
|
||||
backupAllData: function(/**nsIFile*/ file)
|
||||
{
|
||||
FilterStorage.saveToDisk(file);
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes user's custom filters to a file.
|
||||
*/
|
||||
backupCustomFilters: function(/**nsIFile*/ file)
|
||||
{
|
||||
let subscriptions = FilterStorage.subscriptions.filter(function(s) s instanceof SpecialSubscription);
|
||||
let list = ["[Adblock Plus 2.0]"];
|
||||
for (let i = 0; i < subscriptions.length; i++)
|
||||
{
|
||||
let subscription = subscriptions[i];
|
||||
let typeAddition = "";
|
||||
if (subscription.defaults)
|
||||
typeAddition = "/" + subscription.defaults.join("/");
|
||||
list.push("! [" + subscription.title + "]" + typeAddition);
|
||||
for (let j = 0; j < subscription.filters.length; j++)
|
||||
{
|
||||
let filter = subscription.filters[j];
|
||||
// Skip checksums
|
||||
if (filter instanceof CommentFilter && this.CHECKSUM_REGEXP.test(filter.text))
|
||||
continue;
|
||||
// Skip group headers
|
||||
if (filter instanceof CommentFilter && this.GROUPTITLE_REGEXP.test(filter.text))
|
||||
continue;
|
||||
list.push(filter.text);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert checksum. Have to add an empty line to the end of the list to
|
||||
// account for the trailing newline in the file.
|
||||
list.push("");
|
||||
let checksum = Utils.generateChecksum(list);
|
||||
list.pop();
|
||||
if (checksum)
|
||||
list.splice(1, 0, "! Checksum: " + checksum);
|
||||
|
||||
function generator()
|
||||
{
|
||||
for (let i = 0; i < list.length; i++)
|
||||
yield list[i];
|
||||
}
|
||||
|
||||
IO.writeToFile(file, true, generator(), function(e)
|
||||
{
|
||||
if (e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
Utils.alert(window, E("backupButton").getAttribute("_backupError"), E("backupButton").getAttribute("_backupDialogTitle"));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("load", function()
|
||||
{
|
||||
Backup.init();
|
||||
}, false);
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implementation of the various actions performed on the filters.
|
||||
* @class
|
||||
*/
|
||||
var FilterActions =
|
||||
{
|
||||
/**
|
||||
* Initializes filter actions.
|
||||
*/
|
||||
init: function()
|
||||
{
|
||||
let me = this;
|
||||
this.treeElement.parentNode.addEventListener("keypress", function(event)
|
||||
{
|
||||
me.keyPress(event);
|
||||
}, true);
|
||||
this.treeElement.view = FilterView;
|
||||
|
||||
this.treeElement.inputField.addEventListener("keypress", function(event)
|
||||
{
|
||||
// Prevent the tree from capturing cursor keys pressed in the input field
|
||||
if (event.keyCode >= event.DOM_VK_PAGE_UP && event.keyCode <= event.DOM_VK_DOWN)
|
||||
event.stopPropagation();
|
||||
}, false);
|
||||
|
||||
// Create a copy of the view menu
|
||||
function fixId(node, newId)
|
||||
{
|
||||
if (node.nodeType == node.ELEMENT_NODE)
|
||||
{
|
||||
if (node.hasAttribute("id"))
|
||||
node.setAttribute("id", node.getAttribute("id").replace(/\d+$/, newId));
|
||||
|
||||
for (let i = 0, len = node.childNodes.length; i < len; i++)
|
||||
fixId(node.childNodes[i], newId);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
E("viewMenu").appendChild(fixId(E("filters-view-menu1").cloneNode(true), "2"));
|
||||
},
|
||||
|
||||
/**
|
||||
* <tree> element containing the filters.
|
||||
* @type XULElement
|
||||
*/
|
||||
get treeElement() E("filtersTree"),
|
||||
|
||||
/**
|
||||
* Tests whether the tree is currently visible.
|
||||
*/
|
||||
get visible()
|
||||
{
|
||||
return !this.treeElement.parentNode.collapsed;
|
||||
},
|
||||
|
||||
/**
|
||||
* Tests whether the tree is currently focused.
|
||||
* @type Boolean
|
||||
*/
|
||||
get focused()
|
||||
{
|
||||
let focused = document.commandDispatcher.focusedElement;
|
||||
while (focused)
|
||||
{
|
||||
if ("treeBoxObject" in focused && focused.treeBoxObject == FilterView.boxObject)
|
||||
return true;
|
||||
focused = focused.parentNode;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates visible filter commands whenever the selected subscription changes.
|
||||
*/
|
||||
updateCommands: function()
|
||||
{
|
||||
E("filters-add-command").setAttribute("disabled", !FilterView.editable);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called whenever filter actions menu is opened, initializes menu items.
|
||||
*/
|
||||
fillActionsPopup: function()
|
||||
{
|
||||
let editable = FilterView.editable;
|
||||
let items = FilterView.selectedItems.filter(function(i) !i.filter.dummy);
|
||||
items.sort(function(entry1, entry2) entry1.index - entry2.index);
|
||||
let activeItems = items.filter(function(i) i.filter instanceof ActiveFilter);
|
||||
|
||||
E("filters-edit-command").setAttribute("disabled", !editable || !items.length);
|
||||
E("filters-delete-command").setAttribute("disabled", !editable || !items.length);
|
||||
E("filters-resetHitCounts-command").setAttribute("disabled", !activeItems.length);
|
||||
E("filters-moveUp-command").setAttribute("disabled", !editable || FilterView.isSorted() || !items.length || items[0].index == 0);
|
||||
E("filters-moveDown-command").setAttribute("disabled", !editable || FilterView.isSorted() || !items.length || items[items.length - 1].index == FilterView.rowCount - 1);
|
||||
E("filters-copy-command").setAttribute("disabled", !items.length);
|
||||
E("filters-cut-command").setAttribute("disabled", !editable || !items.length);
|
||||
E("filters-paste-command").setAttribute("disabled", !editable || !Utils.clipboard.hasDataMatchingFlavors(["text/unicode"], 1, Utils.clipboard.kGlobalClipboard));
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes sort current order for the tree. Sorts by filter column if the list is unsorted.
|
||||
* @param {String} order either "ascending" or "descending"
|
||||
*/
|
||||
setSortOrder: function(sortOrder)
|
||||
{
|
||||
let col = (FilterView.sortColumn ? FilterView.sortColumn.id : "col-filter");
|
||||
FilterView.sortBy(col, sortOrder);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the visibility of a tree column.
|
||||
*/
|
||||
toggleColumn: function(/**String*/ id)
|
||||
{
|
||||
let col = E(id);
|
||||
col.setAttribute("hidden", col.hidden ? "false" : "true");
|
||||
},
|
||||
|
||||
/**
|
||||
* Enables or disables all filters in the current selection.
|
||||
*/
|
||||
selectionToggleDisabled: function()
|
||||
{
|
||||
if (this.treeElement.editingColumn)
|
||||
return;
|
||||
|
||||
let items = FilterView.selectedItems.filter(function(i) i.filter instanceof ActiveFilter);
|
||||
if (items.length)
|
||||
{
|
||||
FilterView.boxObject.beginUpdateBatch();
|
||||
let newValue = !items[0].filter.disabled;
|
||||
for (let i = 0; i < items.length; i++)
|
||||
items[i].filter.disabled = newValue;
|
||||
FilterView.boxObject.endUpdateBatch();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects all entries in the list.
|
||||
*/
|
||||
selectAll: function()
|
||||
{
|
||||
if (this.treeElement.editingColumn)
|
||||
return;
|
||||
|
||||
FilterView.selection.selectAll();
|
||||
this.treeElement.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts editing the current filter.
|
||||
*/
|
||||
startEditing: function()
|
||||
{
|
||||
if (this.treeElement.editingColumn)
|
||||
return;
|
||||
|
||||
this.treeElement.startEditing(FilterView.selection.currentIndex, FilterView.boxObject.columns.getNamedColumn("col-filter"));
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts editing a new filter at the current position.
|
||||
*/
|
||||
insertFilter: function()
|
||||
{
|
||||
if (!FilterView.editable || this.treeElement.editingColumn)
|
||||
return;
|
||||
|
||||
FilterView.insertEditDummy();
|
||||
this.startEditing();
|
||||
|
||||
let tree = this.treeElement;
|
||||
let listener = function(event)
|
||||
{
|
||||
if (event.attrName == "editing" && tree.editingRow < 0)
|
||||
{
|
||||
tree.removeEventListener("DOMAttrModified", listener, false);
|
||||
FilterView.removeEditDummy();
|
||||
}
|
||||
}
|
||||
tree.addEventListener("DOMAttrModified", listener, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes items from the list.
|
||||
*/
|
||||
deleteItems: function(/**Array*/ items)
|
||||
{
|
||||
let oldIndex = FilterView.selection.currentIndex;
|
||||
items.sort(function(entry1, entry2) entry2.index - entry1.index);
|
||||
|
||||
for (let i = 0; i < items.length; i++)
|
||||
FilterStorage.removeFilter(items[i].filter, FilterView._subscription, items[i].index);
|
||||
|
||||
FilterView.selectRow(oldIndex);
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes selected filters.
|
||||
*/
|
||||
deleteSelected: function()
|
||||
{
|
||||
if (!FilterView.editable || this.treeElement.editingColumn)
|
||||
return;
|
||||
|
||||
let items = FilterView.selectedItems;
|
||||
if (items.length == 0 || (items.length >= 2 && !Utils.confirm(window, this.treeElement.getAttribute("_removewarning"))))
|
||||
return;
|
||||
|
||||
this.deleteItems(items)
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets hit counts of the selected filters.
|
||||
*/
|
||||
resetHitCounts: function()
|
||||
{
|
||||
if (this.treeElement.editingColumn)
|
||||
return;
|
||||
|
||||
let items = FilterView.selectedItems.filter(function(i) i.filter instanceof ActiveFilter);
|
||||
if (items.length)
|
||||
FilterStorage.resetHitCounts(items.map(function(i) i.filter));
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves items to a different position in the list.
|
||||
* @param {Array} items
|
||||
* @param {Integer} offset negative offsets move the items up, positive down
|
||||
*/
|
||||
_moveItems: function(/**Array*/ items, /**Integer*/ offset)
|
||||
{
|
||||
if (!items.length)
|
||||
return;
|
||||
|
||||
if (offset < 0)
|
||||
{
|
||||
items.sort(function(entry1, entry2) entry1.index - entry2.index);
|
||||
let position = items[0].index + offset;
|
||||
if (position < 0)
|
||||
return;
|
||||
|
||||
for (let i = 0; i < items.length; i++)
|
||||
FilterStorage.moveFilter(items[i].filter, FilterView._subscription, items[i].index, position++);
|
||||
FilterView.selection.rangedSelect(position - items.length, position - 1, false);
|
||||
}
|
||||
else if (offset > 0)
|
||||
{
|
||||
items.sort(function(entry1, entry2) entry2.index - entry1.index);
|
||||
let position = items[0].index + offset;
|
||||
if (position >= FilterView.rowCount)
|
||||
return;
|
||||
|
||||
for (let i = 0; i < items.length; i++)
|
||||
FilterStorage.moveFilter(items[i].filter, FilterView._subscription, items[i].index, position--);
|
||||
FilterView.selection.rangedSelect(position + 1, position + items.length, false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves selected filters one line up.
|
||||
*/
|
||||
moveUp: function()
|
||||
{
|
||||
if (!FilterView.editable || FilterView.isEmpty || FilterView.isSorted() || this.treeElement.editingColumn)
|
||||
return;
|
||||
|
||||
this._moveItems(FilterView.selectedItems, -1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves selected filters one line down.
|
||||
*/
|
||||
moveDown: function()
|
||||
{
|
||||
if (!FilterView.editable || FilterView.isEmpty || FilterView.isSorted() || this.treeElement.editingColumn)
|
||||
return;
|
||||
|
||||
this._moveItems(FilterView.selectedItems, 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fills the context menu of the filters columns.
|
||||
*/
|
||||
fillColumnPopup: function(/**Element*/ element)
|
||||
{
|
||||
let suffix = element.id.match(/\d+$/)[0] || "1";
|
||||
|
||||
E("filters-view-filter" + suffix).setAttribute("checked", !E("col-filter").hidden);
|
||||
E("filters-view-slow" + suffix).setAttribute("checked", !E("col-slow").hidden);
|
||||
E("filters-view-enabled" + suffix).setAttribute("checked", !E("col-enabled").hidden);
|
||||
E("filters-view-hitcount" + suffix).setAttribute("checked", !E("col-hitcount").hidden);
|
||||
E("filters-view-lasthit" + suffix).setAttribute("checked", !E("col-lasthit").hidden);
|
||||
|
||||
let sortColumn = FilterView.sortColumn;
|
||||
let sortColumnID = (sortColumn ? sortColumn.id : null);
|
||||
let sortDir = (sortColumn ? sortColumn.getAttribute("sortDirection") : "natural");
|
||||
E("filters-sort-none" + suffix).setAttribute("checked", sortColumn == null);
|
||||
E("filters-sort-filter" + suffix).setAttribute("checked", sortColumnID == "col-filter");
|
||||
E("filters-sort-enabled" + suffix).setAttribute("checked", sortColumnID == "col-enabled");
|
||||
E("filters-sort-hitcount" + suffix).setAttribute("checked", sortColumnID == "col-hitcount");
|
||||
E("filters-sort-lasthit" + suffix).setAttribute("checked", sortColumnID == "col-lasthit");
|
||||
E("filters-sort-asc" + suffix).setAttribute("checked", sortDir == "ascending");
|
||||
E("filters-sort-desc" + suffix).setAttribute("checked", sortDir == "descending");
|
||||
},
|
||||
|
||||
/**
|
||||
* Fills tooltip with the item data.
|
||||
*/
|
||||
fillTooltip: function(event)
|
||||
{
|
||||
let item = FilterView.getItemAt(event.clientX, event.clientY);
|
||||
if (!item || item.filter.dummy)
|
||||
{
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
function setMultilineContent(box, text)
|
||||
{
|
||||
while (box.firstChild)
|
||||
box.removeChild(box.firstChild);
|
||||
|
||||
for (var i = 0; i < text.length; i += 80)
|
||||
{
|
||||
var description = document.createElement("description");
|
||||
description.setAttribute("value", text.substr(i, 80));
|
||||
box.appendChild(description);
|
||||
}
|
||||
}
|
||||
|
||||
setMultilineContent(E("tooltip-filter"), item.filter.text);
|
||||
|
||||
E("tooltip-hitcount-row").hidden = !(item.filter instanceof ActiveFilter);
|
||||
E("tooltip-lasthit-row").hidden = !(item.filter instanceof ActiveFilter) || !item.filter.lastHit;
|
||||
if (item.filter instanceof ActiveFilter)
|
||||
{
|
||||
E("tooltip-hitcount").setAttribute("value", item.filter.hitCount)
|
||||
E("tooltip-lasthit").setAttribute("value", Utils.formatTime(item.filter.lastHit))
|
||||
}
|
||||
|
||||
E("tooltip-additional").hidden = false;
|
||||
if (item.filter instanceof InvalidFilter && item.filter.reason)
|
||||
E("tooltip-additional").textContent = item.filter.reason;
|
||||
else if (item.filter instanceof RegExpFilter && defaultMatcher.isSlowFilter(item.filter))
|
||||
E("tooltip-additional").textContent = Utils.getString("filter_regexp_tooltip");
|
||||
else
|
||||
E("tooltip-additional").hidden = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called whenever a key is pressed on the list.
|
||||
*/
|
||||
keyPress: function(/**Event*/ event)
|
||||
{
|
||||
if (event.target != E("filtersTree"))
|
||||
return;
|
||||
|
||||
let modifiers = 0;
|
||||
if (event.altKey)
|
||||
modifiers |= SubscriptionActions._altMask;
|
||||
if (event.ctrlKey)
|
||||
modifiers |= SubscriptionActions._ctrlMask;
|
||||
if (event.metaKey)
|
||||
modifiers |= SubscriptionActions._metaMask;
|
||||
|
||||
if (event.charCode == " ".charCodeAt(0) && modifiers == 0 && !E("col-enabled").hidden)
|
||||
this.selectionToggleDisabled();
|
||||
else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP && modifiers == SubscriptionActions._accelMask)
|
||||
{
|
||||
E("filters-moveUp-command").doCommand();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN && modifiers == SubscriptionActions._accelMask)
|
||||
{
|
||||
E("filters-moveDown-command").doCommand();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Copies selected items to clipboard and optionally removes them from the
|
||||
* list after that.
|
||||
*/
|
||||
copySelected: function(/**Boolean*/ keep)
|
||||
{
|
||||
let items = FilterView.selectedItems;
|
||||
if (!items.length)
|
||||
return;
|
||||
|
||||
items.sort(function(entry1, entry2) entry1.index - entry2.index);
|
||||
let text = items.map(function(i) i.filter.text).join(IO.lineBreak);
|
||||
Utils.clipboardHelper.copyString(text);
|
||||
|
||||
if (!keep && FilterView.editable && !this.treeElement.editingColumn)
|
||||
this.deleteItems(items);
|
||||
},
|
||||
|
||||
/**
|
||||
* Pastes text from clipboard as filters at the current position.
|
||||
*/
|
||||
paste: function()
|
||||
{
|
||||
if (!FilterView.editable || this.treeElement.editingColumn)
|
||||
return;
|
||||
|
||||
let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
|
||||
transferable.addDataFlavor("text/unicode");
|
||||
|
||||
let data;
|
||||
try
|
||||
{
|
||||
data = {};
|
||||
Utils.clipboard.getData(transferable, Utils.clipboard.kGlobalClipboard);
|
||||
transferable.getTransferData("text/unicode", data, {});
|
||||
data = data.value.QueryInterface(Ci.nsISupportsString).data;
|
||||
}
|
||||
catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
let item = FilterView.currentItem;
|
||||
let position = (item ? item.index : FilterView.data.length);
|
||||
|
||||
let lines = data.replace(/\r/g, "").split("\n");
|
||||
for (let i = 0; i < lines.length; i++)
|
||||
{
|
||||
let filter = Filter.fromText(lines[i]);
|
||||
if (filter)
|
||||
FilterStorage.addFilter(filter, FilterView._subscription, position++);
|
||||
}
|
||||
},
|
||||
|
||||
dragItems: null,
|
||||
|
||||
/**
|
||||
* Called whenever the user starts a drag operation.
|
||||
*/
|
||||
startDrag: function(/**Event*/ event)
|
||||
{
|
||||
let items = FilterView.selectedItems;
|
||||
if (!items.length)
|
||||
return;
|
||||
|
||||
items.sort(function(entry1, entry2) entry1.index - entry2.index);
|
||||
event.dataTransfer.setData("text/plain", items.map(function(i) i.filter.text).join(IO.lineBreak));
|
||||
this.dragItems = items;
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called to check whether moving the items to the given position is possible.
|
||||
*/
|
||||
canDrop: function(/**Integer*/ newPosition, /**nsIDOMDataTransfer*/ dataTransfer)
|
||||
{
|
||||
if (!FilterView.editable || this.treeElement.editingColumn)
|
||||
return false;
|
||||
|
||||
// If we aren't dragging items then maybe we got filters as plain text
|
||||
if (!this.dragItems)
|
||||
return dataTransfer && dataTransfer.getData("text/plain");
|
||||
|
||||
if (FilterView.isEmpty || FilterView.isSorted())
|
||||
return false;
|
||||
|
||||
if (newPosition < this.dragItems[0].index)
|
||||
return true;
|
||||
else if (newPosition > this.dragItems[this.dragItems.length - 1].index + 1)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the user decides to drop the items.
|
||||
*/
|
||||
drop: function(/**Integer*/ newPosition, /**nsIDOMDataTransfer*/ dataTransfer)
|
||||
{
|
||||
if (!FilterView.editable || this.treeElement.editingColumn)
|
||||
return;
|
||||
|
||||
if (!this.dragItems)
|
||||
{
|
||||
// We got filters as plain text, insert them into the list
|
||||
let data = (dataTransfer ? dataTransfer.getData("text/plain") : null);
|
||||
if (data)
|
||||
{
|
||||
let lines = data.replace(/\r/g, "").split("\n");
|
||||
for (let i = 0; i < lines.length; i++)
|
||||
{
|
||||
let filter = Filter.fromText(lines[i]);
|
||||
if (filter)
|
||||
FilterStorage.addFilter(filter, FilterView._subscription, newPosition++);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (FilterView.isEmpty || FilterView.isSorted())
|
||||
return;
|
||||
|
||||
if (newPosition < this.dragItems[0].index)
|
||||
this._moveItems(this.dragItems, newPosition - this.dragItems[0].index);
|
||||
else if (newPosition > this.dragItems[this.dragItems.length - 1].index + 1)
|
||||
this._moveItems(this.dragItems, newPosition - this.dragItems[this.dragItems.length - 1].index - 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called whenever the a drag operation finishes.
|
||||
*/
|
||||
endDrag: function(/**Event*/ event)
|
||||
{
|
||||
this.dragItems = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called if filters have been dragged into a subscription and need to be removed.
|
||||
*/
|
||||
removeDraggedFilters: function()
|
||||
{
|
||||
if (!this.dragItems)
|
||||
return;
|
||||
|
||||
this.deleteItems(this.dragItems);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("load", function()
|
||||
{
|
||||
FilterActions.init();
|
||||
}, false);
|
||||
@@ -0,0 +1,820 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
/**
|
||||
* nsITreeView implementation to display filters of a particular filter
|
||||
* subscription.
|
||||
* @class
|
||||
*/
|
||||
var FilterView =
|
||||
{
|
||||
/**
|
||||
* Initialization function.
|
||||
*/
|
||||
init: function()
|
||||
{
|
||||
if (this.sortProcs)
|
||||
return;
|
||||
|
||||
function compareText(/**Filter*/ filter1, /**Filter*/ filter2)
|
||||
{
|
||||
if (filter1.text < filter2.text)
|
||||
return -1;
|
||||
else if (filter1.text > filter2.text)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
function compareSlow(/**Filter*/ filter1, /**Filter*/ filter2)
|
||||
{
|
||||
let isSlow1 = filter1 instanceof RegExpFilter && defaultMatcher.isSlowFilter(filter1);
|
||||
let isSlow2 = filter2 instanceof RegExpFilter && defaultMatcher.isSlowFilter(filter2);
|
||||
return isSlow1 - isSlow2;
|
||||
}
|
||||
function compareEnabled(/**Filter*/ filter1, /**Filter*/ filter2)
|
||||
{
|
||||
let hasEnabled1 = (filter1 instanceof ActiveFilter ? 1 : 0);
|
||||
let hasEnabled2 = (filter2 instanceof ActiveFilter ? 1 : 0);
|
||||
if (hasEnabled1 != hasEnabled2)
|
||||
return hasEnabled1 - hasEnabled2;
|
||||
else if (hasEnabled1)
|
||||
return (filter2.disabled - filter1.disabled);
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
function compareHitCount(/**Filter*/ filter1, /**Filter*/ filter2)
|
||||
{
|
||||
let hasHitCount1 = (filter1 instanceof ActiveFilter ? 1 : 0);
|
||||
let hasHitCount2 = (filter2 instanceof ActiveFilter ? 1 : 0);
|
||||
if (hasHitCount1 != hasHitCount2)
|
||||
return hasHitCount1 - hasHitCount2;
|
||||
else if (hasHitCount1)
|
||||
return filter1.hitCount - filter2.hitCount;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
function compareLastHit(/**Filter*/ filter1, /**Filter*/ filter2)
|
||||
{
|
||||
let hasLastHit1 = (filter1 instanceof ActiveFilter ? 1 : 0);
|
||||
let hasLastHit2 = (filter2 instanceof ActiveFilter ? 1 : 0);
|
||||
if (hasLastHit1 != hasLastHit2)
|
||||
return hasLastHit1 - hasLastHit2;
|
||||
else if (hasLastHit1)
|
||||
return filter1.lastHit - filter2.lastHit;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a sort function from a primary and a secondary comparison function.
|
||||
* @param {Function} cmpFunc comparison function to be called first
|
||||
* @param {Function} fallbackFunc (optional) comparison function to be called if primary function returns 0
|
||||
* @param {Boolean} desc if true, the result of the primary function (not the secondary function) will be reversed - sorting in descending order
|
||||
* @result {Function} comparison function to be used
|
||||
*/
|
||||
function createSortFunction(cmpFunc, fallbackFunc, desc)
|
||||
{
|
||||
let factor = (desc ? -1 : 1);
|
||||
|
||||
return function(entry1, entry2)
|
||||
{
|
||||
// Comment replacements not bound to a filter always go last
|
||||
let isLast1 = ("origFilter" in entry1 && entry1.filter == null);
|
||||
let isLast2 = ("origFilter" in entry2 && entry2.filter == null);
|
||||
if (isLast1)
|
||||
return (isLast2 ? 0 : 1)
|
||||
else if (isLast2)
|
||||
return -1;
|
||||
|
||||
let ret = cmpFunc(entry1.filter, entry2.filter);
|
||||
if (ret == 0 && fallbackFunc)
|
||||
return fallbackFunc(entry1.filter, entry2.filter);
|
||||
else
|
||||
return factor * ret;
|
||||
}
|
||||
}
|
||||
|
||||
this.sortProcs = {
|
||||
filter: createSortFunction(compareText, null, false),
|
||||
filterDesc: createSortFunction(compareText, null, true),
|
||||
slow: createSortFunction(compareSlow, compareText, true),
|
||||
slowDesc: createSortFunction(compareSlow, compareText, false),
|
||||
enabled: createSortFunction(compareEnabled, compareText, false),
|
||||
enabledDesc: createSortFunction(compareEnabled, compareText, true),
|
||||
hitcount: createSortFunction(compareHitCount, compareText, false),
|
||||
hitcountDesc: createSortFunction(compareHitCount, compareText, true),
|
||||
lasthit: createSortFunction(compareLastHit, compareText, false),
|
||||
lasthitDesc: createSortFunction(compareLastHit, compareText, true)
|
||||
};
|
||||
|
||||
let me = this;
|
||||
let proxy = function()
|
||||
{
|
||||
return me._onChange.apply(me, arguments);
|
||||
};
|
||||
FilterNotifier.addListener(proxy);
|
||||
window.addEventListener("unload", function()
|
||||
{
|
||||
FilterNotifier.removeListener(proxy);
|
||||
}, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter change processing.
|
||||
* @see FilterNotifier.addListener()
|
||||
*/
|
||||
_onChange: function(action, item, param1, param2, param3)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "subscription.updated":
|
||||
{
|
||||
if (item == this._subscription)
|
||||
this.refresh(true);
|
||||
break;
|
||||
}
|
||||
case "filter.disabled":
|
||||
case "filter.hitCount":
|
||||
case "filter.lastHit":
|
||||
{
|
||||
this.updateFilter(item);
|
||||
break;
|
||||
}
|
||||
case "filter.added":
|
||||
{
|
||||
let subscription = param1;
|
||||
let position = param2;
|
||||
if (subscription == this._subscription)
|
||||
this.addFilterAt(position, item);
|
||||
break;
|
||||
}
|
||||
case "filter.removed":
|
||||
{
|
||||
let subscription = param1;
|
||||
let position = param2;
|
||||
if (subscription == this._subscription)
|
||||
this.removeFilterAt(position);
|
||||
break;
|
||||
}
|
||||
case "filter.moved":
|
||||
{
|
||||
let subscription = param1;
|
||||
let oldPosition = param2;
|
||||
let newPosition = param3;
|
||||
if (subscription == this._subscription)
|
||||
this.moveFilterAt(oldPosition, newPosition);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Box object of the tree that this view is attached to.
|
||||
* @type nsITreeBoxObject
|
||||
*/
|
||||
boxObject: null,
|
||||
|
||||
/**
|
||||
* Map of used cell properties to the corresponding nsIAtom representations.
|
||||
*/
|
||||
atoms: null,
|
||||
|
||||
/**
|
||||
* "Filter" to be displayed if no filter group is selected.
|
||||
*/
|
||||
noGroupDummy: null,
|
||||
|
||||
/**
|
||||
* "Filter" to be displayed if the selected group is empty.
|
||||
*/
|
||||
noFiltersDummy: null,
|
||||
|
||||
/**
|
||||
* "Filter" to be displayed for a new filter being edited.
|
||||
*/
|
||||
editDummy: null,
|
||||
|
||||
/**
|
||||
* Displayed list of filters, might be sorted.
|
||||
* @type Filter[]
|
||||
*/
|
||||
data: [],
|
||||
|
||||
/**
|
||||
* <tree> element that the view is attached to.
|
||||
* @type XULElement
|
||||
*/
|
||||
get treeElement() this.boxObject ? this.boxObject.treeBody.parentNode : null,
|
||||
|
||||
/**
|
||||
* Checks whether the list is currently empty (regardless of dummy entries).
|
||||
* @type Boolean
|
||||
*/
|
||||
get isEmpty()
|
||||
{
|
||||
return !this._subscription || !this._subscription.filters.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether the filters in the view can be changed.
|
||||
* @type Boolean
|
||||
*/
|
||||
get editable()
|
||||
{
|
||||
return (FilterView._subscription instanceof SpecialSubscription);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns current item of the list.
|
||||
* @type Object
|
||||
*/
|
||||
get currentItem()
|
||||
{
|
||||
let index = this.selection.currentIndex;
|
||||
if (index >= 0 && index < this.data.length)
|
||||
return this.data[index];
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns items that are currently selected in the list.
|
||||
* @type Object[]
|
||||
*/
|
||||
get selectedItems()
|
||||
{
|
||||
let items = []
|
||||
for (let i = 0; i < this.selection.getRangeCount(); i++)
|
||||
{
|
||||
let min = {};
|
||||
let max = {};
|
||||
this.selection.getRangeAt(i, min, max);
|
||||
for (let j = min.value; j <= max.value; j++)
|
||||
if (j >= 0 && j < this.data.length)
|
||||
items.push(this.data[j]);
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
getItemAt: function(x, y)
|
||||
{
|
||||
let row = this.boxObject.getRowAt(x, y);
|
||||
if (row >= 0 && row < this.data.length)
|
||||
return this.data[row];
|
||||
else
|
||||
return null;
|
||||
},
|
||||
|
||||
_subscription: 0,
|
||||
|
||||
/**
|
||||
* Filter subscription being displayed.
|
||||
* @type Subscription
|
||||
*/
|
||||
get subscription() this._subscription,
|
||||
set subscription(value)
|
||||
{
|
||||
if (value == this._subscription)
|
||||
return;
|
||||
|
||||
// Make sure the editor is done before we update the list.
|
||||
if (this.treeElement)
|
||||
this.treeElement.stopEditing(true);
|
||||
|
||||
this._subscription = value;
|
||||
this.refresh(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Will be true if updates are outstanding because the list was hidden.
|
||||
*/
|
||||
_dirty: false,
|
||||
|
||||
/**
|
||||
* Updates internal view data after a change.
|
||||
* @param {Boolean} force if false, a refresh will only happen if previous
|
||||
* changes were suppressed because the list was hidden
|
||||
*/
|
||||
refresh: function(force)
|
||||
{
|
||||
if (FilterActions.visible)
|
||||
{
|
||||
if (!force && !this._dirty)
|
||||
return;
|
||||
this._dirty = false;
|
||||
this.updateData();
|
||||
this.selectRow(0);
|
||||
}
|
||||
else
|
||||
this._dirty = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Map of comparison functions by column ID or column ID + "Desc" for
|
||||
* descending sort order.
|
||||
* @const
|
||||
*/
|
||||
sortProcs: null,
|
||||
|
||||
/**
|
||||
* Column that the list is currently sorted on.
|
||||
* @type Element
|
||||
*/
|
||||
sortColumn: null,
|
||||
|
||||
/**
|
||||
* Sorting function currently in use.
|
||||
* @type Function
|
||||
*/
|
||||
sortProc: null,
|
||||
|
||||
/**
|
||||
* Resorts the list.
|
||||
* @param {String} col ID of the column to sort on. If null, the natural order is restored.
|
||||
* @param {String} direction "ascending" or "descending", if null the sort order is toggled.
|
||||
*/
|
||||
sortBy: function(col, direction)
|
||||
{
|
||||
let newSortColumn = null;
|
||||
if (col)
|
||||
{
|
||||
newSortColumn = this.boxObject.columns.getNamedColumn(col).element;
|
||||
if (!direction)
|
||||
{
|
||||
if (this.sortColumn == newSortColumn)
|
||||
direction = (newSortColumn.getAttribute("sortDirection") == "ascending" ? "descending" : "ascending");
|
||||
else
|
||||
direction = "ascending";
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sortColumn && this.sortColumn != newSortColumn)
|
||||
this.sortColumn.removeAttribute("sortDirection");
|
||||
|
||||
this.sortColumn = newSortColumn;
|
||||
if (this.sortColumn)
|
||||
{
|
||||
this.sortColumn.setAttribute("sortDirection", direction);
|
||||
this.sortProc = this.sortProcs[col.replace(/^col-/, "") + (direction == "descending" ? "Desc" : "")];
|
||||
}
|
||||
else
|
||||
this.sortProc = null;
|
||||
|
||||
if (this.data.length > 1)
|
||||
{
|
||||
this.updateData();
|
||||
this.boxObject.invalidate();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts dummy entry into the list if necessary.
|
||||
*/
|
||||
addDummyRow: function()
|
||||
{
|
||||
if (this.boxObject && this.data.length == 0)
|
||||
{
|
||||
if (this._subscription)
|
||||
this.data.splice(0, 0, this.noFiltersDummy);
|
||||
else
|
||||
this.data.splice(0, 0, this.noGroupDummy);
|
||||
this.boxObject.rowCountChanged(0, 1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes dummy entry from the list if present.
|
||||
*/
|
||||
removeDummyRow: function()
|
||||
{
|
||||
if (this.boxObject && this.isEmpty && this.data.length)
|
||||
{
|
||||
this.data.splice(0, 1);
|
||||
this.boxObject.rowCountChanged(0, -1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts dummy row when a new filter is being edited.
|
||||
*/
|
||||
insertEditDummy: function()
|
||||
{
|
||||
FilterView.removeDummyRow();
|
||||
let position = this.selection.currentIndex;
|
||||
if (position >= this.data.length)
|
||||
position = this.data.length - 1;
|
||||
if (position < 0)
|
||||
position = 0;
|
||||
|
||||
this.editDummy.index = (position < this.data.length ? this.data[position].index : this.data.length);
|
||||
this.editDummy.position = position;
|
||||
this.data.splice(position, 0, this.editDummy);
|
||||
this.boxObject.rowCountChanged(position, 1);
|
||||
this.selectRow(position);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes dummy row once the edit is finished.
|
||||
*/
|
||||
removeEditDummy: function()
|
||||
{
|
||||
let position = this.editDummy.position;
|
||||
if (typeof position != "undefined" && position < this.data.length && this.data[position] == this.editDummy)
|
||||
{
|
||||
this.data.splice(position, 1);
|
||||
this.boxObject.rowCountChanged(position, -1);
|
||||
FilterView.addDummyRow();
|
||||
|
||||
this.selectRow(position);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects a row in the tree and makes sure it is visible.
|
||||
*/
|
||||
selectRow: function(row)
|
||||
{
|
||||
if (this.selection)
|
||||
{
|
||||
row = Math.min(Math.max(row, 0), this.data.length - 1);
|
||||
this.selection.select(row);
|
||||
this.boxObject.ensureRowIsVisible(row);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds a particular filter in the list and selects it.
|
||||
*/
|
||||
selectFilter: function(/**Filter*/ filter)
|
||||
{
|
||||
let index = -1;
|
||||
for (let i = 0; i < this.data.length; i++)
|
||||
{
|
||||
if (this.data[i].filter == filter)
|
||||
{
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= 0)
|
||||
{
|
||||
this.selectRow(index);
|
||||
this.treeElement.focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates value of data property on sorting or filter subscription changes.
|
||||
*/
|
||||
updateData: function()
|
||||
{
|
||||
let oldCount = this.rowCount;
|
||||
if (this._subscription && this._subscription.filters.length)
|
||||
{
|
||||
this.data = this._subscription.filters.map(function(f, i) ({index: i, filter: f}));
|
||||
if (this.sortProc)
|
||||
{
|
||||
// Hide comments in the list, they should be sorted like the filter following them
|
||||
let followingFilter = null;
|
||||
for (let i = this.data.length - 1; i >= 0; i--)
|
||||
{
|
||||
if (this.data[i].filter instanceof CommentFilter)
|
||||
{
|
||||
this.data[i].origFilter = this.data[i].filter;
|
||||
this.data[i].filter = followingFilter;
|
||||
}
|
||||
else
|
||||
followingFilter = this.data[i].filter;
|
||||
}
|
||||
|
||||
this.data.sort(this.sortProc);
|
||||
|
||||
// Restore comments
|
||||
for (let i = 0; i < this.data.length; i++)
|
||||
{
|
||||
if ("origFilter" in this.data[i])
|
||||
{
|
||||
this.data[i].filter = this.data[i].origFilter;
|
||||
delete this.data[i].origFilter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
this.data = [];
|
||||
|
||||
if (this.boxObject)
|
||||
{
|
||||
this.boxObject.rowCountChanged(0, -oldCount);
|
||||
this.boxObject.rowCountChanged(0, this.rowCount);
|
||||
}
|
||||
|
||||
this.addDummyRow();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called to update the view when a filter property is changed.
|
||||
*/
|
||||
updateFilter: function(/**Filter*/ filter)
|
||||
{
|
||||
for (let i = 0; i < this.data.length; i++)
|
||||
if (this.data[i].filter == filter)
|
||||
this.boxObject.invalidateRow(i);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called if a filter has been inserted at the specified position.
|
||||
*/
|
||||
addFilterAt: function(/**Integer*/ position, /**Filter*/ filter)
|
||||
{
|
||||
if (this.data.length == 1 && this.data[0].filter.dummy)
|
||||
{
|
||||
this.data.splice(0, 1);
|
||||
this.boxObject.rowCountChanged(0, -1);
|
||||
}
|
||||
|
||||
if (this.sortProc)
|
||||
{
|
||||
this.updateData();
|
||||
for (let i = 0; i < this.data.length; i++)
|
||||
{
|
||||
if (this.data[i].index == position)
|
||||
{
|
||||
position = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (let i = 0; i < this.data.length; i++)
|
||||
if (this.data[i].index >= position)
|
||||
this.data[i].index++;
|
||||
this.data.splice(position, 0, {index: position, filter: filter});
|
||||
}
|
||||
this.boxObject.rowCountChanged(position, 1);
|
||||
this.selectRow(position);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called if a filter has been removed at the specified position.
|
||||
*/
|
||||
removeFilterAt: function(/**Integer*/ position)
|
||||
{
|
||||
for (let i = 0; i < this.data.length; i++)
|
||||
{
|
||||
if (this.data[i].index == position)
|
||||
{
|
||||
this.data.splice(i, 1);
|
||||
this.boxObject.rowCountChanged(i, -1);
|
||||
i--;
|
||||
}
|
||||
else if (this.data[i].index > position)
|
||||
this.data[i].index--;
|
||||
}
|
||||
this.addDummyRow();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called if a filter has been moved within the list.
|
||||
*/
|
||||
moveFilterAt: function(/**Integer*/ oldPosition, /**Integer*/ newPosition)
|
||||
{
|
||||
let dir = (oldPosition < newPosition ? 1 : -1);
|
||||
for (let i = 0; i < this.data.length; i++)
|
||||
{
|
||||
if (this.data[i].index == oldPosition)
|
||||
this.data[i].index = newPosition;
|
||||
else if (dir * this.data[i].index > dir * oldPosition && dir * this.data[i].index <= dir * newPosition)
|
||||
this.data[i].index -= dir;
|
||||
}
|
||||
|
||||
if (!this.sortProc)
|
||||
{
|
||||
let item = this.data[oldPosition];
|
||||
this.data.splice(oldPosition, 1);
|
||||
this.data.splice(newPosition, 0, item);
|
||||
this.boxObject.invalidateRange(Math.min(oldPosition, newPosition), Math.max(oldPosition, newPosition));
|
||||
}
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]),
|
||||
|
||||
setTree: function(boxObject)
|
||||
{
|
||||
this.init();
|
||||
this.boxObject = boxObject;
|
||||
|
||||
if (this.boxObject)
|
||||
{
|
||||
this.noGroupDummy = {index: 0, filter: {text: this.boxObject.treeBody.getAttribute("noGroupText"), dummy: true}};
|
||||
this.noFiltersDummy = {index: 0, filter: {text: this.boxObject.treeBody.getAttribute("noFiltersText"), dummy: true}};
|
||||
this.editDummy = {filter: {text: ""}};
|
||||
|
||||
let atomService = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
|
||||
let stringAtoms = ["col-filter", "col-enabled", "col-hitcount", "col-lasthit", "type-comment", "type-filterlist", "type-whitelist", "type-elemhide", "type-invalid"];
|
||||
let boolAtoms = ["selected", "dummy", "slow", "disabled"];
|
||||
|
||||
this.atoms = {};
|
||||
for each (let atom in stringAtoms)
|
||||
this.atoms[atom] = atomService.getAtom(atom);
|
||||
for each (let atom in boolAtoms)
|
||||
{
|
||||
this.atoms[atom + "-true"] = atomService.getAtom(atom + "-true");
|
||||
this.atoms[atom + "-false"] = atomService.getAtom(atom + "-false");
|
||||
}
|
||||
|
||||
let columns = this.boxObject.columns;
|
||||
for (let i = 0; i < columns.length; i++)
|
||||
if (columns[i].element.hasAttribute("sortDirection"))
|
||||
this.sortBy(columns[i].id, columns[i].element.getAttribute("sortDirection"));
|
||||
|
||||
this.refresh(true);
|
||||
}
|
||||
},
|
||||
|
||||
selection: null,
|
||||
|
||||
get rowCount() this.data.length,
|
||||
|
||||
getCellText: function(row, col)
|
||||
{
|
||||
if (row < 0 || row >= this.data.length)
|
||||
return null;
|
||||
|
||||
col = col.id;
|
||||
if (col != "col-filter" && col != "col-slow" && col != "col-hitcount" && col != "col-lasthit")
|
||||
return null;
|
||||
|
||||
let filter = this.data[row].filter;
|
||||
if (col == "col-filter")
|
||||
return filter.text;
|
||||
else if (col == "col-slow")
|
||||
return (filter instanceof RegExpFilter && defaultMatcher.isSlowFilter(filter) ? "!" : null);
|
||||
else if (filter instanceof ActiveFilter)
|
||||
{
|
||||
if (col == "col-hitcount")
|
||||
return filter.hitCount;
|
||||
else if (col == "col-lasthit")
|
||||
return (filter.lastHit ? Utils.formatTime(filter.lastHit) : null);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
generateProperties: function(list, properties)
|
||||
{
|
||||
if (properties)
|
||||
{
|
||||
// Gecko 21 and below: we have an nsISupportsArray parameter, add atoms
|
||||
// to that.
|
||||
for (let i = 0; i < list.length; i++)
|
||||
if (list[i] in this.atoms)
|
||||
properties.AppendElement(this.atoms[list[i]]);
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Gecko 22+: no parameter, just return a string
|
||||
return list.join(" ");
|
||||
}
|
||||
},
|
||||
|
||||
getColumnProperties: function(col, properties)
|
||||
{
|
||||
return this.generateProperties(["col-" + col.id], properties);
|
||||
},
|
||||
|
||||
getRowProperties: function(row, properties)
|
||||
{
|
||||
if (row < 0 || row >= this.data.length)
|
||||
return "";
|
||||
|
||||
let list = [];
|
||||
let filter = this.data[row].filter;
|
||||
list.push("selected-" + this.selection.isSelected(row));
|
||||
list.push("slow-" + (filter instanceof RegExpFilter && defaultMatcher.isSlowFilter(filter)));
|
||||
if (filter instanceof ActiveFilter)
|
||||
list.push("disabled-" + filter.disabled);
|
||||
list.push("dummy-" + ("dummy" in filter));
|
||||
|
||||
if (filter instanceof CommentFilter)
|
||||
list.push("type-comment");
|
||||
else if (filter instanceof BlockingFilter)
|
||||
list.push("type-filterlist");
|
||||
else if (filter instanceof WhitelistFilter)
|
||||
list.push("type-whitelist");
|
||||
else if (filter instanceof ElemHideFilter)
|
||||
list.push("type-elemhide");
|
||||
else if (filter instanceof InvalidFilter)
|
||||
list.push("type-invalid");
|
||||
|
||||
return this.generateProperties(list, properties);
|
||||
},
|
||||
|
||||
getCellProperties: function(row, col, properties)
|
||||
{
|
||||
return this.getRowProperties(row, properties) + " " + this.getColumnProperties(col, properties);
|
||||
},
|
||||
|
||||
cycleHeader: function(col)
|
||||
{
|
||||
let oldDirection = col.element.getAttribute("sortDirection");
|
||||
if (oldDirection == "ascending")
|
||||
this.sortBy(col.id, "descending");
|
||||
else if (oldDirection == "descending")
|
||||
this.sortBy(null, null);
|
||||
else
|
||||
this.sortBy(col.id, "ascending");
|
||||
},
|
||||
|
||||
isSorted: function()
|
||||
{
|
||||
return (this.sortProc != null);
|
||||
},
|
||||
|
||||
canDrop: function(row, orientation, dataTransfer)
|
||||
{
|
||||
if (orientation == Ci.nsITreeView.DROP_ON || row < 0 || row >= this.data.length || !this.editable)
|
||||
return false;
|
||||
|
||||
let item = this.data[row];
|
||||
let position = (orientation == Ci.nsITreeView.DROP_BEFORE ? item.index : item.index + 1);
|
||||
return FilterActions.canDrop(position, dataTransfer);
|
||||
},
|
||||
|
||||
drop: function(row, orientation, dataTransfer)
|
||||
{
|
||||
if (orientation == Ci.nsITreeView.DROP_ON || row < 0 || row >= this.data.length || !this.editable)
|
||||
return;
|
||||
|
||||
let item = this.data[row];
|
||||
let position = (orientation == Ci.nsITreeView.DROP_BEFORE ? item.index : item.index + 1);
|
||||
FilterActions.drop(position, dataTransfer);
|
||||
},
|
||||
|
||||
isEditable: function(row, col)
|
||||
{
|
||||
if (row < 0 || row >= this.data.length || !this.editable)
|
||||
return false;
|
||||
|
||||
let filter = this.data[row].filter;
|
||||
if (col.id == "col-filter")
|
||||
return !("dummy" in filter);
|
||||
else
|
||||
return false;
|
||||
},
|
||||
|
||||
setCellText: function(row, col, value)
|
||||
{
|
||||
if (row < 0 || row >= this.data.length || col.id != "col-filter")
|
||||
return;
|
||||
|
||||
let oldFilter = this.data[row].filter;
|
||||
let position = this.data[row].index;
|
||||
value = Filter.normalize(value);
|
||||
if (!value || value == oldFilter.text)
|
||||
return;
|
||||
|
||||
// Make sure we don't get called recursively (see https://adblockplus.org/forum/viewtopic.php?t=9003)
|
||||
this.treeElement.stopEditing();
|
||||
|
||||
let newFilter = Filter.fromText(value);
|
||||
if (this.data[row] == this.editDummy)
|
||||
this.removeEditDummy();
|
||||
else
|
||||
FilterStorage.removeFilter(oldFilter, this._subscription, position);
|
||||
FilterStorage.addFilter(newFilter, this._subscription, position);
|
||||
},
|
||||
|
||||
cycleCell: function(row, col)
|
||||
{
|
||||
if (row < 0 || row >= this.data.length || col.id != "col-enabled")
|
||||
return;
|
||||
|
||||
let filter = this.data[row].filter;
|
||||
if (filter instanceof ActiveFilter)
|
||||
filter.disabled = !filter.disabled;
|
||||
},
|
||||
|
||||
isContainer: function(row) false,
|
||||
isContainerOpen: function(row) false,
|
||||
isContainerEmpty: function(row) true,
|
||||
getLevel: function(row) 0,
|
||||
getParentIndex: function(row) -1,
|
||||
hasNextSibling: function(row, afterRow) false,
|
||||
toggleOpenState: function(row) {},
|
||||
getProgressMode: function() null,
|
||||
getImageSrc: function() null,
|
||||
isSeparator: function() false,
|
||||
performAction: function() {},
|
||||
performActionOnRow: function() {},
|
||||
performActionOnCell: function() {},
|
||||
getCellValue: function() null,
|
||||
setCellValue: function() {},
|
||||
selectionChanged: function() {},
|
||||
};
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implementation of the filter search functionality.
|
||||
* @class
|
||||
*/
|
||||
var FilterSearch =
|
||||
{
|
||||
/**
|
||||
* Initializes findbar widget.
|
||||
*/
|
||||
init: function()
|
||||
{
|
||||
let filters = E("filtersTree");
|
||||
for (let prop in FilterSearch.fakeBrowser)
|
||||
filters[prop] = FilterSearch.fakeBrowser[prop];
|
||||
Object.defineProperty(filters, "_lastSearchString", {
|
||||
get: function()
|
||||
{
|
||||
return this.finder.searchString;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
let findbar = E("findbar");
|
||||
findbar.browser = filters;
|
||||
|
||||
findbar.addEventListener("keypress", function(event)
|
||||
{
|
||||
// Work-around for bug 490047
|
||||
if (event.keyCode == KeyEvent.DOM_VK_RETURN)
|
||||
event.preventDefault();
|
||||
}, false);
|
||||
|
||||
// Hack to prevent "highlight all" from getting enabled
|
||||
findbar.toggleHighlight = function() {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs a text search.
|
||||
* @param {String} text text to be searched
|
||||
* @param {Integer} direction search direction: -1 (backwards), 0 (forwards
|
||||
* starting with current), 1 (forwards starting with next)
|
||||
* @param {Boolean} caseSensitive if true, a case-sensitive search is performed
|
||||
* @result {Integer} one of the nsITypeAheadFind constants
|
||||
*/
|
||||
search: function(text, direction, caseSensitive)
|
||||
{
|
||||
function normalizeString(string) caseSensitive ? string : string.toLowerCase();
|
||||
|
||||
function findText(text, direction, startIndex)
|
||||
{
|
||||
let list = E("filtersTree");
|
||||
let col = list.columns.getNamedColumn("col-filter");
|
||||
let count = list.view.rowCount;
|
||||
for (let i = startIndex + direction; i >= 0 && i < count; i += (direction || 1))
|
||||
{
|
||||
let filter = normalizeString(list.view.getCellText(i, col));
|
||||
if (filter.indexOf(text) >= 0)
|
||||
{
|
||||
FilterView.selectRow(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
text = normalizeString(text);
|
||||
|
||||
// First try to find the entry in the current list
|
||||
if (findText(text, direction, E("filtersTree").currentIndex))
|
||||
return Ci.nsITypeAheadFind.FIND_FOUND;
|
||||
|
||||
// Now go through the other subscriptions
|
||||
let result = Ci.nsITypeAheadFind.FIND_FOUND;
|
||||
let subscriptions = FilterStorage.subscriptions.slice();
|
||||
subscriptions.sort((s1, s2) => (s1 instanceof SpecialSubscription) - (s2 instanceof SpecialSubscription));
|
||||
let current = subscriptions.indexOf(FilterView.subscription);
|
||||
direction = direction || 1;
|
||||
for (let i = current + direction; ; i+= direction)
|
||||
{
|
||||
if (i < 0)
|
||||
{
|
||||
i = subscriptions.length - 1;
|
||||
result = Ci.nsITypeAheadFind.FIND_WRAPPED;
|
||||
}
|
||||
else if (i >= subscriptions.length)
|
||||
{
|
||||
i = 0;
|
||||
result = Ci.nsITypeAheadFind.FIND_WRAPPED;
|
||||
}
|
||||
if (i == current)
|
||||
break;
|
||||
|
||||
let subscription = subscriptions[i];
|
||||
for (let j = 0; j < subscription.filters.length; j++)
|
||||
{
|
||||
let filter = normalizeString(subscription.filters[j].text);
|
||||
if (filter.indexOf(text) >= 0)
|
||||
{
|
||||
let list = E(subscription instanceof SpecialSubscription ? "groups" : "subscriptions");
|
||||
let node = Templater.getNodeForData(list, "subscription", subscription);
|
||||
if (!node)
|
||||
break;
|
||||
|
||||
// Select subscription in its list and restore focus after that
|
||||
let oldFocus = document.commandDispatcher.focusedElement;
|
||||
E("tabs").selectedIndex = (subscription instanceof SpecialSubscription ? 1 : 0);
|
||||
list.ensureElementIsVisible(node);
|
||||
list.selectItem(node);
|
||||
if (oldFocus)
|
||||
{
|
||||
oldFocus.focus();
|
||||
Utils.runAsync(() => oldFocus.focus());
|
||||
}
|
||||
|
||||
Utils.runAsync(() => findText(text, direction, direction == 1 ? -1 : subscription.filters.length));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ci.nsITypeAheadFind.FIND_NOTFOUND;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fake browser implementation to make findbar widget happy - searches in
|
||||
* the filter list.
|
||||
*/
|
||||
FilterSearch.fakeBrowser =
|
||||
{
|
||||
finder:
|
||||
{
|
||||
_resultListeners: [],
|
||||
searchString: null,
|
||||
caseSensitive: false,
|
||||
lastResult: null,
|
||||
|
||||
_notifyResultListeners: function(result, findBackwards)
|
||||
{
|
||||
this.lastResult = result;
|
||||
for (let listener of this._resultListeners)
|
||||
{
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=958101, starting
|
||||
// with Gecko 29 only one parameter is expected.
|
||||
try
|
||||
{
|
||||
if (listener.onFindResult.length == 1)
|
||||
{
|
||||
listener.onFindResult({searchString: this.searchString,
|
||||
result: result, findBackwards: findBackwards});
|
||||
}
|
||||
else
|
||||
listener.onFindResult(result, findBackwards);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
fastFind: function(searchString, linksOnly, drawOutline)
|
||||
{
|
||||
this.searchString = searchString;
|
||||
let result = FilterSearch.search(this.searchString, 0,
|
||||
this.caseSensitive);
|
||||
this._notifyResultListeners(result, false);
|
||||
},
|
||||
|
||||
findAgain: function(findBackwards, linksOnly, drawOutline)
|
||||
{
|
||||
let result = FilterSearch.search(this.searchString,
|
||||
findBackwards ? -1 : 1,
|
||||
this.caseSensitive);
|
||||
this._notifyResultListeners(result, findBackwards);
|
||||
},
|
||||
|
||||
addResultListener: function(listener)
|
||||
{
|
||||
if (this._resultListeners.indexOf(listener) === -1)
|
||||
this._resultListeners.push(listener);
|
||||
},
|
||||
|
||||
removeResultListener: function(listener)
|
||||
{
|
||||
let index = this._resultListeners.indexOf(listener);
|
||||
if (index !== -1)
|
||||
this._resultListeners.splice(index, 1);
|
||||
},
|
||||
|
||||
// Irrelevant for us
|
||||
requestMatchesCount: function(searchString, matchLimit, linksOnly) {},
|
||||
highlight: function(highlight, word) {},
|
||||
enableSelection: function() {},
|
||||
removeSelection: function() {},
|
||||
focusContent: function() {},
|
||||
keyPress: function() {}
|
||||
},
|
||||
|
||||
currentURI: Utils.makeURI("http://example.com/"),
|
||||
contentWindow:
|
||||
{
|
||||
focus: function()
|
||||
{
|
||||
E("filtersTree").focus();
|
||||
},
|
||||
scrollByLines: function(num)
|
||||
{
|
||||
E("filtersTree").boxObject.scrollByLines(num);
|
||||
},
|
||||
scrollByPages: function(num)
|
||||
{
|
||||
E("filtersTree").boxObject.scrollByPages(num);
|
||||
},
|
||||
},
|
||||
|
||||
messageManager:
|
||||
{
|
||||
_messageMap: {
|
||||
"Findbar:Mouseup": "mouseup",
|
||||
"Findbar:Keypress": "keypress"
|
||||
},
|
||||
|
||||
_messageFromEvent: function(event)
|
||||
{
|
||||
for (let message in this._messageMap)
|
||||
if (this._messageMap[message] == event.type)
|
||||
return {target: event.currentTarget, name: message, data: event};
|
||||
return null;
|
||||
},
|
||||
|
||||
addMessageListener: function(message, listener)
|
||||
{
|
||||
if (!this._messageMap.hasOwnProperty(message))
|
||||
return;
|
||||
|
||||
if (!("_ABPHandler" in listener))
|
||||
listener._ABPHandler = (event) => listener.receiveMessage(this._messageFromEvent(event));
|
||||
|
||||
E("filtersTree").addEventListener(this._messageMap[message], listener._ABPHandler, false);
|
||||
},
|
||||
|
||||
removeMessageListener: function(message, listener)
|
||||
{
|
||||
if (this._messageMap.hasOwnProperty(message) && listener._ABPHandler)
|
||||
E("filtersTree").removeEventListener(this._messageMap[message], listener._ABPHandler, false);
|
||||
},
|
||||
|
||||
sendAsyncMessage: function() {}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("load", function()
|
||||
{
|
||||
FilterSearch.init();
|
||||
}, false);
|
||||
@@ -0,0 +1,592 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implemetation of the various actions that can be performed on subscriptions.
|
||||
* @class
|
||||
*/
|
||||
var SubscriptionActions =
|
||||
{
|
||||
/**
|
||||
* Returns the subscription list currently having focus if any.
|
||||
* @type Element
|
||||
*/
|
||||
get focusedList()
|
||||
{
|
||||
return E("tabs").selectedPanel.getElementsByTagName("richlistbox")[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the currently selected and focused subscription item if any.
|
||||
* @type Element
|
||||
*/
|
||||
get selectedItem()
|
||||
{
|
||||
let list = this.focusedList;
|
||||
return (list ? list.selectedItem : null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds the subscription for a particular filter, selects it and selects the
|
||||
* filter.
|
||||
*/
|
||||
selectFilter: function(/**Filter*/ filter)
|
||||
{
|
||||
let node = null;
|
||||
let tabIndex = -1;
|
||||
let subscriptions = filter.subscriptions.slice();
|
||||
subscriptions.sort(function(s1, s2) s1.disabled - s2.disabled);
|
||||
for (let i = 0; i < subscriptions.length; i++)
|
||||
{
|
||||
let subscription = subscriptions[i];
|
||||
let list = E(subscription instanceof SpecialSubscription ? "groups" : "subscriptions");
|
||||
tabIndex = (subscription instanceof SpecialSubscription ? 1 : 0);
|
||||
node = Templater.getNodeForData(list, "subscription", subscription);
|
||||
if (node)
|
||||
break;
|
||||
}
|
||||
if (node)
|
||||
{
|
||||
E("tabs").selectedIndex = tabIndex;
|
||||
Utils.runAsync(function()
|
||||
{
|
||||
node.parentNode.ensureElementIsVisible(node);
|
||||
node.parentNode.selectItem(node);
|
||||
if (!FilterActions.visible)
|
||||
E("subscription-showHideFilters-command").doCommand();
|
||||
Utils.runAsync(FilterView.selectFilter, FilterView, filter);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates subscription commands whenever the selected subscription changes.
|
||||
* Note: this method might be called with a wrong "this" value.
|
||||
*/
|
||||
updateCommands: function()
|
||||
{
|
||||
let node = SubscriptionActions.selectedItem;
|
||||
let data = Templater.getDataForNode(node);
|
||||
let subscription = (data ? data.subscription : null)
|
||||
E("subscription-editTitle-command").setAttribute("disabled", !subscription ||
|
||||
subscription.fixedTitle);
|
||||
E("subscription-update-command").setAttribute("disabled", !subscription ||
|
||||
!(subscription instanceof DownloadableSubscription) ||
|
||||
Synchronizer.isExecuting(subscription.url));
|
||||
E("subscription-moveUp-command").setAttribute("disabled", !subscription ||
|
||||
!node || !node.previousSibling || !!node.previousSibling.id);
|
||||
E("subscription-moveDown-command").setAttribute("disabled", !subscription ||
|
||||
!node || !node.nextSibling || !!node.nextSibling.id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts title editing for the selected subscription.
|
||||
*/
|
||||
editTitle: function()
|
||||
{
|
||||
let node = this.selectedItem;
|
||||
if (node)
|
||||
TitleEditor.start(node);
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers re-download of a filter subscription.
|
||||
*/
|
||||
updateFilters: function(/**Node*/ node)
|
||||
{
|
||||
let data = Templater.getDataForNode(node || this.selectedItem);
|
||||
if (data && data.subscription instanceof DownloadableSubscription)
|
||||
Synchronizer.execute(data.subscription, true, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers re-download of all filter subscriptions.
|
||||
*/
|
||||
updateAllFilters: function()
|
||||
{
|
||||
for (let i = 0; i < FilterStorage.subscriptions.length; i++)
|
||||
{
|
||||
let subscription = FilterStorage.subscriptions[i];
|
||||
if (subscription instanceof DownloadableSubscription)
|
||||
Synchronizer.execute(subscription, true, true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets Subscription.disabled field to a new value.
|
||||
*/
|
||||
setDisabled: function(/**Element*/ node, /**Boolean*/ value)
|
||||
{
|
||||
let data = Templater.getDataForNode(node || this.selectedItem);
|
||||
if (data)
|
||||
data.subscription.disabled = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enables all disabled filters in a subscription.
|
||||
*/
|
||||
enableFilters: function(/**Element*/ node)
|
||||
{
|
||||
let data = Templater.getDataForNode(node);
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
let filters = data.subscription.filters;
|
||||
for (let i = 0, l = filters.length; i < l; i++)
|
||||
if (filters[i] instanceof ActiveFilter && filters[i].disabled)
|
||||
filters[i].disabled = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a filter subscription from the list (after a warning).
|
||||
*/
|
||||
remove: function(/**Node*/ node)
|
||||
{
|
||||
let data = Templater.getDataForNode(node || this.selectedItem);
|
||||
if (data && Utils.confirm(window, Utils.getString(data.subscription instanceof SpecialSubscription ? "remove_group_warning" : "remove_subscription_warning")))
|
||||
FilterStorage.removeSubscription(data.subscription);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new filter group and allows the user to change its title.
|
||||
*/
|
||||
addGroup: function()
|
||||
{
|
||||
let subscription = SpecialSubscription.create();
|
||||
FilterStorage.addSubscription(subscription);
|
||||
|
||||
let list = E("groups");
|
||||
let node = Templater.getNodeForData(list, "subscription", subscription);
|
||||
if (node)
|
||||
{
|
||||
list.focus();
|
||||
list.ensureElementIsVisible(node);
|
||||
list.selectedItem = node;
|
||||
this.editTitle();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves a filter subscription one line up.
|
||||
*/
|
||||
moveUp: function(/**Node*/ node)
|
||||
{
|
||||
node = Templater.getDataNode(node || this.selectedItem);
|
||||
let data = Templater.getDataForNode(node);
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
let previousData = Templater.getDataForNode(node.previousSibling);
|
||||
if (!previousData)
|
||||
return;
|
||||
|
||||
FilterStorage.moveSubscription(data.subscription, previousData.subscription);
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves a filter subscription one line down.
|
||||
*/
|
||||
moveDown: function(/**Node*/ node)
|
||||
{
|
||||
node = Templater.getDataNode(node || this.selectedItem);
|
||||
let data = Templater.getDataForNode(node);
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
let nextNode = node.nextSibling;
|
||||
if (!Templater.getDataForNode(nextNode))
|
||||
return;
|
||||
|
||||
let nextData = Templater.getDataForNode(nextNode.nextSibling);
|
||||
FilterStorage.moveSubscription(data.subscription, nextData ? nextData.subscription : null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the context menu for a subscription node.
|
||||
*/
|
||||
openMenu: function(/**Event*/ event, /**Node*/ node)
|
||||
{
|
||||
node.getElementsByClassName("actionMenu")[0].openPopupAtScreen(event.screenX, event.screenY, true);
|
||||
},
|
||||
|
||||
_altMask: 2,
|
||||
_ctrlMask: 4,
|
||||
_metaMask: 8,
|
||||
get _accelMask()
|
||||
{
|
||||
let result = this._ctrlMask;
|
||||
try {
|
||||
let accelKey = Utils.prefService.getIntPref("ui.key.accelKey");
|
||||
if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_META)
|
||||
result = this._metaMask;
|
||||
else if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_ALT)
|
||||
result = this._altMask;
|
||||
} catch(e) {}
|
||||
this.__defineGetter__("_accelMask", function() result);
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a key is pressed on the subscription list.
|
||||
*/
|
||||
keyPress: function(/**Event*/ event)
|
||||
{
|
||||
let modifiers = 0;
|
||||
if (event.altKey)
|
||||
modifiers |= this._altMask;
|
||||
if (event.ctrlKey)
|
||||
modifiers |= this._ctrlMask;
|
||||
if (event.metaKey)
|
||||
modifiers |= this._metaMask;
|
||||
|
||||
if (event.charCode == " ".charCodeAt(0) && modifiers == 0)
|
||||
{
|
||||
// Ignore if Space is pressed on a button
|
||||
for (let node = event.target; node; node = node.parentNode)
|
||||
if (node.localName == "button")
|
||||
return;
|
||||
|
||||
let data = Templater.getDataForNode(this.selectedItem);
|
||||
if (data)
|
||||
data.subscription.disabled = !data.subscription.disabled;
|
||||
}
|
||||
else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP && modifiers == this._accelMask)
|
||||
{
|
||||
E("subscription-moveUp-command").doCommand();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN && modifiers == this._accelMask)
|
||||
{
|
||||
E("subscription-moveDown-command").doCommand();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscription currently being dragged if any.
|
||||
* @type Subscription
|
||||
*/
|
||||
dragSubscription: null,
|
||||
|
||||
/**
|
||||
* Called when a subscription entry is dragged.
|
||||
*/
|
||||
startDrag: function(/**Event*/ event, /**Node*/ node)
|
||||
{
|
||||
let data = Templater.getDataForNode(node);
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
event.dataTransfer.addElement(node);
|
||||
event.dataTransfer.setData("text/x-moz-url", data.subscription.url);
|
||||
event.dataTransfer.setData("text/plain", data.subscription.title);
|
||||
this.dragSubscription = data.subscription;
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when something is dragged over a subscription entry or subscriptions list.
|
||||
*/
|
||||
dragOver: function(/**Event*/ event)
|
||||
{
|
||||
// Don't allow dragging onto a scroll bar
|
||||
for (let node = event.originalTarget; node; node = node.parentNode)
|
||||
if (node.localName == "scrollbar")
|
||||
return;
|
||||
|
||||
// Don't allow dragging onto element's borders
|
||||
let target = event.originalTarget;
|
||||
while (target && target.localName != "richlistitem")
|
||||
target = target.parentNode;
|
||||
if (!target)
|
||||
target = event.originalTarget;
|
||||
|
||||
let styles = window.getComputedStyle(target, null);
|
||||
let rect = target.getBoundingClientRect();
|
||||
if (event.clientX < rect.left + parseInt(styles.borderLeftWidth, 10) ||
|
||||
event.clientY < rect.top + parseInt(styles.borderTopWidth, 10) ||
|
||||
event.clientX > rect.right - parseInt(styles.borderRightWidth, 10) - 1 ||
|
||||
event.clientY > rect.bottom - parseInt(styles.borderBottomWidth, 10) - 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If not dragging a subscription check whether we can accept plain text
|
||||
if (!this.dragSubscription)
|
||||
{
|
||||
let data = Templater.getDataForNode(event.target);
|
||||
if (!data || !(data.subscription instanceof SpecialSubscription) || !event.dataTransfer.getData("text/plain"))
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when something is dropped on a subscription entry or subscriptions list.
|
||||
*/
|
||||
drop: function(/**Event*/ event, /**Node*/ node)
|
||||
{
|
||||
if (!this.dragSubscription)
|
||||
{
|
||||
// Not dragging a subscription, maybe this is plain text that we can add as filters?
|
||||
let data = Templater.getDataForNode(node);
|
||||
if (data && data.subscription instanceof SpecialSubscription)
|
||||
{
|
||||
let lines = event.dataTransfer.getData("text/plain").replace(/\r/g, "").split("\n");
|
||||
for (let i = 0; i < lines.length; i++)
|
||||
{
|
||||
let filter = Filter.fromText(lines[i]);
|
||||
if (filter)
|
||||
FilterStorage.addFilter(filter, data.subscription);
|
||||
}
|
||||
FilterActions.removeDraggedFilters();
|
||||
event.stopPropagation();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// When dragging down we need to insert after the drop node, otherwise before it.
|
||||
node = Templater.getDataNode(node);
|
||||
if (node)
|
||||
{
|
||||
let dragNode = Templater.getNodeForData(node.parentNode, "subscription", this.dragSubscription);
|
||||
if (node.compareDocumentPosition(dragNode) & node.DOCUMENT_POSITION_PRECEDING)
|
||||
node = node.nextSibling;
|
||||
}
|
||||
|
||||
let data = Templater.getDataForNode(node);
|
||||
FilterStorage.moveSubscription(this.dragSubscription, data ? data.subscription : null);
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the drag operation for a subscription is finished.
|
||||
*/
|
||||
endDrag: function()
|
||||
{
|
||||
this.dragSubscription = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscription title editing functionality.
|
||||
* @class
|
||||
*/
|
||||
var TitleEditor =
|
||||
{
|
||||
/**
|
||||
* List item corresponding with the currently edited subscription if any.
|
||||
* @type Node
|
||||
*/
|
||||
subscriptionEdited: null,
|
||||
|
||||
/**
|
||||
* Starts editing of a subscription title.
|
||||
* @param {Node} node subscription list entry or a child node
|
||||
* @param {Boolean} [checkSelection] if true the editor will not start if the
|
||||
* item was selected in the preceding mousedown event
|
||||
*/
|
||||
start: function(node, checkSelection)
|
||||
{
|
||||
if (this.subscriptionEdited)
|
||||
this.end(true);
|
||||
|
||||
let subscriptionNode = Templater.getDataNode(node);
|
||||
if (!subscriptionNode || (checkSelection && !subscriptionNode._wasSelected))
|
||||
return;
|
||||
|
||||
let subscription = Templater.getDataForNode(subscriptionNode).subscription;
|
||||
if (!subscription || subscription.fixedTitle)
|
||||
return;
|
||||
|
||||
subscriptionNode.getElementsByClassName("titleBox")[0].selectedIndex = 1;
|
||||
let editor = subscriptionNode.getElementsByClassName("titleEditor")[0];
|
||||
editor.value = subscription.title;
|
||||
editor.setSelectionRange(0, editor.value.length);
|
||||
this.subscriptionEdited = subscriptionNode;
|
||||
editor.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops editing of a subscription title.
|
||||
* @param {Boolean} save if true the entered value will be saved, otherwise dismissed
|
||||
*/
|
||||
end: function(save)
|
||||
{
|
||||
if (!this.subscriptionEdited)
|
||||
return;
|
||||
|
||||
let subscriptionNode = this.subscriptionEdited;
|
||||
this.subscriptionEdited = null;
|
||||
|
||||
let newTitle = null;
|
||||
if (save)
|
||||
{
|
||||
newTitle = subscriptionNode.getElementsByClassName("titleEditor")[0].value;
|
||||
newTitle = newTitle.replace(/^\s+/, "").replace(/\s+$/, "");
|
||||
}
|
||||
|
||||
let subscription = Templater.getDataForNode(subscriptionNode).subscription
|
||||
if (newTitle && newTitle != subscription.title)
|
||||
subscription.title = newTitle;
|
||||
else
|
||||
{
|
||||
subscriptionNode.getElementsByClassName("titleBox")[0].selectedIndex = 0;
|
||||
subscriptionNode.parentNode.focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Processes keypress events on the subscription title editor field.
|
||||
*/
|
||||
keyPress: function(/**Event*/ event)
|
||||
{
|
||||
// Prevent any key presses from triggering outside actions
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.keyCode == event.DOM_VK_RETURN || event.keyCode == event.DOM_VK_ENTER)
|
||||
{
|
||||
event.preventDefault();
|
||||
this.end(true);
|
||||
}
|
||||
else if (event.keyCode == event.DOM_VK_CANCEL || event.keyCode == event.DOM_VK_ESCAPE)
|
||||
{
|
||||
event.preventDefault();
|
||||
this.end(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Methods called when choosing and adding a new filter subscription.
|
||||
* @class
|
||||
*/
|
||||
var SelectSubscription =
|
||||
{
|
||||
/**
|
||||
* Starts selection of a filter subscription to add.
|
||||
*/
|
||||
start: function(/**Event*/ event)
|
||||
{
|
||||
let panel = E("selectSubscriptionPanel");
|
||||
let list = E("selectSubscription");
|
||||
let template = E("selectSubscriptionTemplate");
|
||||
let parent = list.menupopup;
|
||||
|
||||
if (panel.state == "open")
|
||||
{
|
||||
list.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove existing entries if any
|
||||
while (parent.lastChild)
|
||||
parent.removeChild(parent.lastChild);
|
||||
|
||||
// Load data
|
||||
let request = new XMLHttpRequest();
|
||||
request.open("GET", "subscriptions.xml");
|
||||
request.onload = function()
|
||||
{
|
||||
// Avoid race condition if two downloads are started in parallel
|
||||
if (panel.state == "open")
|
||||
return;
|
||||
|
||||
// Add subscription entries to the list
|
||||
let subscriptions = request.responseXML.getElementsByTagName("subscription");
|
||||
let listedSubscriptions = [];
|
||||
for (let i = 0; i < subscriptions.length; i++)
|
||||
{
|
||||
let subscription = subscriptions[i];
|
||||
let url = subscription.getAttribute("url");
|
||||
if (!url || url in FilterStorage.knownSubscriptions)
|
||||
continue;
|
||||
|
||||
let localePrefix = Utils.checkLocalePrefixMatch(subscription.getAttribute("prefixes"));
|
||||
let node = Templater.process(template, {
|
||||
__proto__: null,
|
||||
node: subscription,
|
||||
localePrefix: localePrefix
|
||||
});
|
||||
parent.appendChild(node);
|
||||
listedSubscriptions.push(subscription);
|
||||
}
|
||||
let selectedNode = Utils.chooseFilterSubscription(listedSubscriptions);
|
||||
list.selectedItem = Templater.getNodeForData(parent, "node", selectedNode) || parent.firstChild;
|
||||
|
||||
// Show panel and focus list
|
||||
let position = "bottomcenter topleft";
|
||||
panel.openPopup(E("selectSubscriptionButton"), position, 0, 0, false, false, event);
|
||||
Utils.runAsync(list.focus, list);
|
||||
};
|
||||
request.send();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds filter subscription that is selected.
|
||||
*/
|
||||
add: function()
|
||||
{
|
||||
E("selectSubscriptionPanel").hidePopup();
|
||||
|
||||
let data = Templater.getDataForNode(E("selectSubscription").selectedItem);
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
let subscription = Subscription.fromURL(data.node.getAttribute("url"));
|
||||
if (!subscription)
|
||||
return;
|
||||
|
||||
FilterStorage.addSubscription(subscription);
|
||||
subscription.disabled = false;
|
||||
subscription.title = data.node.getAttribute("title");
|
||||
subscription.homepage = data.node.getAttribute("homepage");
|
||||
|
||||
// Make sure the subscription is visible and selected
|
||||
let list = E("subscriptions");
|
||||
let node = Templater.getNodeForData(list, "subscription", subscription);
|
||||
if (node)
|
||||
{
|
||||
list.ensureElementIsVisible(node);
|
||||
list.selectedItem = node;
|
||||
list.focus();
|
||||
}
|
||||
|
||||
// Trigger download if necessary
|
||||
if (subscription instanceof DownloadableSubscription && !subscription.lastDownload)
|
||||
Synchronizer.execute(subscription);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called if the user chooses to view the complete subscriptions list.
|
||||
*/
|
||||
chooseOther: function()
|
||||
{
|
||||
E("selectSubscriptionPanel").hidePopup();
|
||||
window.openDialog("subscriptionSelection.xul", "_blank", "chrome,centerscreen,modal,resizable,dialog=no", null, null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called for keys pressed on the subscription selection panel.
|
||||
*/
|
||||
keyPress: function(/**Event*/ event)
|
||||
{
|
||||
// Buttons and text links handle Enter key themselves
|
||||
if (event.target.localName == "button" || event.target.localName == "label")
|
||||
return;
|
||||
|
||||
if (event.keyCode == event.DOM_VK_RETURN || event.keyCode == event.DOM_VK_ENTER)
|
||||
{
|
||||
// This shouldn't accept our dialog, only the panel
|
||||
event.preventDefault();
|
||||
E("selectSubscriptionAccept").doCommand();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fills a list of filter groups and keeps it updated.
|
||||
* @param {Element} list richlistbox element to be filled
|
||||
* @param {Node} template template to use for the groups
|
||||
* @param {Function} filter filter to decide which lists should be included
|
||||
* @param {Function} listener function to be called on changes
|
||||
* @constructor
|
||||
*/
|
||||
function ListManager(list, template, filter, listener)
|
||||
{
|
||||
this._list = list;
|
||||
this._template = template;
|
||||
this._filter = filter;
|
||||
this._listener = listener || function(){};
|
||||
|
||||
this._deck = this._list.parentNode;
|
||||
|
||||
this._list.listManager = this;
|
||||
this.reload();
|
||||
|
||||
let me = this;
|
||||
let proxy = function()
|
||||
{
|
||||
return me._onChange.apply(me, arguments);
|
||||
};
|
||||
FilterNotifier.addListener(proxy);
|
||||
window.addEventListener("unload", function()
|
||||
{
|
||||
FilterNotifier.removeListener(proxy);
|
||||
}, false);
|
||||
}
|
||||
ListManager.prototype =
|
||||
{
|
||||
/**
|
||||
* List element being managed.
|
||||
* @type Element
|
||||
*/
|
||||
_list: null,
|
||||
/**
|
||||
* Template used for the groups.
|
||||
* @type Node
|
||||
*/
|
||||
_template: null,
|
||||
/**
|
||||
* Filter function to decide which subscriptions should be included.
|
||||
* @type Function
|
||||
*/
|
||||
_filter: null,
|
||||
/**
|
||||
* Function to be called whenever list contents change.
|
||||
* @type Function
|
||||
*/
|
||||
_listener: null,
|
||||
/**
|
||||
* Deck switching between list display and "no entries" message.
|
||||
* @type Element
|
||||
*/
|
||||
_deck: null,
|
||||
|
||||
/**
|
||||
* Completely rebuilds the list.
|
||||
*/
|
||||
reload: function()
|
||||
{
|
||||
// Remove existing entries if any
|
||||
while (this._list.firstChild)
|
||||
this._list.removeChild(this._list.firstChild);
|
||||
|
||||
// Now add all subscriptions
|
||||
let subscriptions = FilterStorage.subscriptions.filter(this._filter, this);
|
||||
if (subscriptions.length)
|
||||
{
|
||||
for each (let subscription in subscriptions)
|
||||
this.addSubscription(subscription, null);
|
||||
|
||||
// Make sure first list item is selected after list initialization
|
||||
Utils.runAsync(function()
|
||||
{
|
||||
this._list.selectItem(this._list.getItemAtIndex(this._list.getIndexOfFirstVisibleRow()));
|
||||
}, this);
|
||||
}
|
||||
|
||||
this._deck.selectedIndex = (subscriptions.length ? 1 : 0);
|
||||
this._listener();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a filter subscription to the list.
|
||||
*/
|
||||
addSubscription: function(/**Subscription*/ subscription, /**Node*/ insertBefore) /**Node*/
|
||||
{
|
||||
let disabledFilters = 0;
|
||||
for (let i = 0, l = subscription.filters.length; i < l; i++)
|
||||
if (subscription.filters[i] instanceof ActiveFilter && subscription.filters[i].disabled)
|
||||
disabledFilters++;
|
||||
|
||||
let node = Templater.process(this._template, {
|
||||
__proto__: null,
|
||||
subscription: subscription,
|
||||
isExternal: subscription instanceof ExternalSubscription,
|
||||
downloading: Synchronizer.isExecuting(subscription.url),
|
||||
disabledFilters: disabledFilters
|
||||
});
|
||||
if (insertBefore)
|
||||
this._list.insertBefore(node, insertBefore);
|
||||
else
|
||||
this._list.appendChild(node);
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Map indicating subscriptions that need their "disabledFilters" property to
|
||||
* be updated by next updateDisabled() call.
|
||||
* @type Object
|
||||
*/
|
||||
_scheduledUpdateDisabled: null,
|
||||
|
||||
/**
|
||||
* Updates subscriptions that had some of their filters enabled/disabled.
|
||||
*/
|
||||
updateDisabled: function()
|
||||
{
|
||||
let list = this._scheduledUpdateDisabled;
|
||||
this._scheduledUpdateDisabled = null;
|
||||
for (let url in list)
|
||||
{
|
||||
let subscription = Subscription.fromURL(url);
|
||||
let subscriptionNode = Templater.getNodeForData(this._list, "subscription", subscription);
|
||||
if (subscriptionNode)
|
||||
{
|
||||
let data = Templater.getDataForNode(subscriptionNode);
|
||||
let disabledFilters = 0;
|
||||
for (let i = 0, l = subscription.filters.length; i < l; i++)
|
||||
if (subscription.filters[i] instanceof ActiveFilter && subscription.filters[i].disabled)
|
||||
disabledFilters++;
|
||||
|
||||
if (disabledFilters != data.disabledFilters)
|
||||
{
|
||||
data.disabledFilters = disabledFilters;
|
||||
Templater.update(this._template, subscriptionNode);
|
||||
|
||||
if (!document.commandDispatcher.focusedElement)
|
||||
this._list.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscriptions change processing.
|
||||
* @see FilterNotifier.addListener()
|
||||
*/
|
||||
_onChange: function(action, item, param1, param2)
|
||||
{
|
||||
|
||||
if (action == "filter.disabled")
|
||||
{
|
||||
if (this._scheduledUpdateDisabled == null)
|
||||
{
|
||||
this._scheduledUpdateDisabled = {__proto__: null};
|
||||
Utils.runAsync(this.updateDisabled, this);
|
||||
}
|
||||
for (let i = 0; i < item.subscriptions.length; i++)
|
||||
this._scheduledUpdateDisabled[item.subscriptions[i].url] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (action != "load" && !this._filter(item))
|
||||
return;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "load":
|
||||
{
|
||||
this.reload();
|
||||
break;
|
||||
}
|
||||
case "subscription.added":
|
||||
{
|
||||
let index = FilterStorage.subscriptions.indexOf(item);
|
||||
if (index >= 0)
|
||||
{
|
||||
let insertBefore = null;
|
||||
for (index++; index < FilterStorage.subscriptions.length && !insertBefore; index++)
|
||||
insertBefore = Templater.getNodeForData(this._list, "subscription", FilterStorage.subscriptions[index]);
|
||||
this.addSubscription(item, insertBefore);
|
||||
this._deck.selectedIndex = 1;
|
||||
this._listener();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "subscription.removed":
|
||||
{
|
||||
let node = Templater.getNodeForData(this._list, "subscription", item);
|
||||
if (node)
|
||||
{
|
||||
let newSelection = node.nextSibling || node.previousSibling;
|
||||
node.parentNode.removeChild(node);
|
||||
if (!this._list.firstChild)
|
||||
{
|
||||
this._deck.selectedIndex = 0;
|
||||
this._list.selectedIndex = -1;
|
||||
}
|
||||
else if (newSelection)
|
||||
{
|
||||
this._list.ensureElementIsVisible(newSelection);
|
||||
this._list.selectedItem = newSelection;
|
||||
}
|
||||
this._listener();
|
||||
}
|
||||
break
|
||||
}
|
||||
case "subscription.moved":
|
||||
{
|
||||
let node = Templater.getNodeForData(this._list, "subscription", item);
|
||||
if (node)
|
||||
{
|
||||
node.parentNode.removeChild(node);
|
||||
let insertBefore = null;
|
||||
let index = FilterStorage.subscriptions.indexOf(item);
|
||||
if (index >= 0)
|
||||
for (index++; index < FilterStorage.subscriptions.length && !insertBefore; index++)
|
||||
insertBefore = Templater.getNodeForData(this._list, "subscription", FilterStorage.subscriptions[index]);
|
||||
this._list.insertBefore(node, insertBefore);
|
||||
this._list.ensureElementIsVisible(node);
|
||||
this._listener();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "subscription.title":
|
||||
case "subscription.disabled":
|
||||
case "subscription.homepage":
|
||||
case "subscription.lastDownload":
|
||||
case "subscription.downloadStatus":
|
||||
{
|
||||
let subscriptionNode = Templater.getNodeForData(this._list, "subscription", item);
|
||||
if (subscriptionNode)
|
||||
{
|
||||
Templater.getDataForNode(subscriptionNode).downloading = Synchronizer.isExecuting(item.url);
|
||||
Templater.update(this._template, subscriptionNode);
|
||||
|
||||
if (!document.commandDispatcher.focusedElement)
|
||||
this._list.focus();
|
||||
this._listener();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "subscription.fixedTitle":
|
||||
{
|
||||
SubscriptionActions.updateCommands();
|
||||
break;
|
||||
}
|
||||
case "subscription.updated":
|
||||
{
|
||||
if (this._scheduledUpdateDisabled == null)
|
||||
{
|
||||
this._scheduledUpdateDisabled = {__proto__: null};
|
||||
Utils.runAsync(this.updateDisabled, this);
|
||||
}
|
||||
this._scheduledUpdateDisabled[item.url] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches list managers to the lists.
|
||||
*/
|
||||
ListManager.init = function()
|
||||
{
|
||||
new ListManager(E("subscriptions"),
|
||||
E("subscriptionTemplate"),
|
||||
function(s) s instanceof RegularSubscription,
|
||||
SubscriptionActions.updateCommands);
|
||||
new ListManager(E("groups"),
|
||||
E("groupTemplate"),
|
||||
function(s) s instanceof SpecialSubscription,
|
||||
SubscriptionActions.updateCommands);
|
||||
};
|
||||
|
||||
window.addEventListener("load", ListManager.init, false);
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Initialization function, called when the window is loaded.
|
||||
*/
|
||||
function init()
|
||||
{
|
||||
if (window.arguments && window.arguments.length)
|
||||
{
|
||||
let filter = window.arguments[0].wrappedJSObject;
|
||||
if (filter instanceof Filter)
|
||||
Utils.runAsync(SubscriptionActions.selectFilter, SubscriptionActions, filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever the currently selected tab changes.
|
||||
*/
|
||||
function onTabChange(/**Element*/ tabbox)
|
||||
{
|
||||
updateSelectedSubscription();
|
||||
|
||||
Utils.runAsync(function()
|
||||
{
|
||||
let panel = tabbox.selectedPanel;
|
||||
if (panel)
|
||||
panel.getElementsByClassName("initialFocus")[0].focus();
|
||||
SubscriptionActions.updateCommands();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever the selected subscription changes.
|
||||
*/
|
||||
function onSelectionChange(/**Element*/ list)
|
||||
{
|
||||
SubscriptionActions.updateCommands();
|
||||
updateSelectedSubscription();
|
||||
list.focus();
|
||||
|
||||
// Take elements of the previously selected item out of the tab order
|
||||
if ("previousSelection" in list && list.previousSelection)
|
||||
{
|
||||
let elements = list.previousSelection.getElementsByClassName("tabable");
|
||||
for (let i = 0; i < elements.length; i++)
|
||||
elements[i].setAttribute("tabindex", "-1");
|
||||
}
|
||||
// Put elements of the selected item into tab order
|
||||
if (list.selectedItem)
|
||||
{
|
||||
let elements = list.selectedItem.getElementsByClassName("tabable");
|
||||
for (let i = 0; i < elements.length; i++)
|
||||
elements[i].removeAttribute("tabindex");
|
||||
}
|
||||
list.previousSelection = list.selectedItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when splitter state changes to make sure it is persisted properly.
|
||||
*/
|
||||
function onSplitterStateChange(/**Element*/ splitter)
|
||||
{
|
||||
let state = splitter.getAttribute("state");
|
||||
if (!state)
|
||||
{
|
||||
splitter.setAttribute("state", "open");
|
||||
document.persist(splitter.id, "state");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates filter list when selected subscription changes.
|
||||
*/
|
||||
function updateSelectedSubscription()
|
||||
{
|
||||
let panel = E("tabs").selectedPanel;
|
||||
if (!panel)
|
||||
return;
|
||||
|
||||
let list = panel.getElementsByTagName("richlistbox")[0];
|
||||
if (!list)
|
||||
return;
|
||||
|
||||
let data = Templater.getDataForNode(list.selectedItem);
|
||||
FilterView.subscription = (data ? data.subscription : null);
|
||||
FilterActions.updateCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Template processing functions.
|
||||
* @class
|
||||
*/
|
||||
var Templater =
|
||||
{
|
||||
/**
|
||||
* Processes a template node using given data object.
|
||||
*/
|
||||
process: function(/**Node*/ template, /**Object*/ data) /**Node*/
|
||||
{
|
||||
// Use a sandbox to resolve attributes (for convenience, not security)
|
||||
let sandbox = Cu.Sandbox(window);
|
||||
for (let key in data)
|
||||
sandbox[key] = data[key];
|
||||
sandbox.formatTime = Utils.formatTime;
|
||||
|
||||
// Clone template but remove id/hidden attributes from it
|
||||
let result = template.cloneNode(true);
|
||||
result.removeAttribute("id");
|
||||
result.removeAttribute("hidden");
|
||||
result._data = data;
|
||||
|
||||
// Resolve any attributes of the for attr="{obj.foo}"
|
||||
let conditionals = [];
|
||||
let nodeIterator = document.createNodeIterator(result, NodeFilter.SHOW_ELEMENT, null, false);
|
||||
for (let node = nodeIterator.nextNode(); node; node = nodeIterator.nextNode())
|
||||
{
|
||||
if (node.localName == "if")
|
||||
conditionals.push(node);
|
||||
for (let i = 0; i < node.attributes.length; i++)
|
||||
{
|
||||
let attribute = node.attributes[i];
|
||||
let len = attribute.value.length;
|
||||
if (len >= 2 && attribute.value[0] == "{" && attribute.value[len - 1] == "}")
|
||||
attribute.value = Cu.evalInSandbox(attribute.value.substr(1, len - 2), sandbox);
|
||||
}
|
||||
}
|
||||
|
||||
// Process <if> tags - remove if condition is false, replace by their children
|
||||
// if it is true
|
||||
for each (let node in conditionals)
|
||||
{
|
||||
let fragment = document.createDocumentFragment();
|
||||
let condition = node.getAttribute("condition");
|
||||
if (condition == "false")
|
||||
condition = false;
|
||||
for (let i = 0; i < node.childNodes.length; i++)
|
||||
{
|
||||
let child = node.childNodes[i];
|
||||
if (child.localName == "elif" || child.localName == "else")
|
||||
{
|
||||
if (condition)
|
||||
break;
|
||||
condition = (child.localName == "elif" ? child.getAttribute("condition") : true);
|
||||
if (condition == "false")
|
||||
condition = false;
|
||||
}
|
||||
else if (condition)
|
||||
fragment.appendChild(node.childNodes[i--]);
|
||||
}
|
||||
node.parentNode.replaceChild(fragment, node);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates first child of a processed template if the underlying data changed.
|
||||
*/
|
||||
update: function(/**Node*/ template, /**Node*/ node)
|
||||
{
|
||||
if (!("_data" in node))
|
||||
return;
|
||||
let newChild = Templater.process(template.firstChild, node._data);
|
||||
delete newChild._data;
|
||||
node.replaceChild(newChild, node.firstChild);
|
||||
},
|
||||
|
||||
/**
|
||||
* Walks up the parent chain for a node until the node corresponding with a
|
||||
* template is found.
|
||||
*/
|
||||
getDataNode: function(/**Node*/ node) /**Node*/
|
||||
{
|
||||
while (node)
|
||||
{
|
||||
if ("_data" in node)
|
||||
return node;
|
||||
node = node.parentNode;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the data used to generate the node from a template.
|
||||
*/
|
||||
getDataForNode: function(/**Node*/ node) /**Object*/
|
||||
{
|
||||
node = Templater.getDataNode(node);
|
||||
if (node)
|
||||
return node._data;
|
||||
else
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a node that has been generated from a template using a particular
|
||||
* data object.
|
||||
*/
|
||||
getNodeForData: function(/**Node*/ parent, /**String*/ property, /**Object*/ data) /**Node*/
|
||||
{
|
||||
for (let child = parent.firstChild; child; child = child.nextSibling)
|
||||
if ("_data" in child && property in child._data && child._data[property] == data)
|
||||
return child;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,395 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://@ADDON_CHROME_NAME@/skin/filters.css" type="text/css"?>
|
||||
|
||||
<!DOCTYPE dialog SYSTEM "chrome://@ADDON_CHROME_NAME@/locale/filters.dtd">
|
||||
|
||||
<dialog
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
title="&dialog.title;"
|
||||
id="abpFiltersWindow"
|
||||
onload="init()"
|
||||
buttons="accept"
|
||||
width="950"
|
||||
height="450"
|
||||
persist="screenX screenY width height sizemode"
|
||||
windowtype="abp:filters">
|
||||
|
||||
<script type="application/x-javascript;version=1.7" src="utils.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="filters.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="filters-subscriptionview.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="filters-subscriptionactions.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="filters-filterview.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="filters-filteractions.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="filters-backup.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="filters-search.js"/>
|
||||
|
||||
<keyset id="filtersKeyset">
|
||||
<key id="subscription-update-key" key="T" modifiers="accel" command="subscription-update-command"/>
|
||||
<key id="subscription-update-all-key" key="T" modifiers="accel,shift" command="subscription-update-all-command"/>
|
||||
<key id="edit-key" keycode="VK_F2" oncommand="E(FilterActions.focused ? 'filters-edit-command' : 'subscription-editTitle-command').doCommand();"/>
|
||||
<key id="delete-key" keycode="VK_DELETE" oncommand="E(FilterActions.focused ? 'filters-delete-command' : 'subscription-delete-command').doCommand();"/>
|
||||
<key id="subscription-showHideFilters-key" key="R" modifiers="accel" command="subscription-showHideFilters-command"/>
|
||||
<key id="moveUp-key" keycode="VK_UP" modifiers="accel"/>
|
||||
<key id="moveDown-key" keycode="VK_DOWN" modifiers="accel"/>
|
||||
<key id="filters-add-key" keycode="VK_INSERT" oncommand="E('filters-add-command').doCommand();"/>
|
||||
<key id="filters-selectAll-key" key="A" modifiers="accel" oncommand="if (FilterActions.focused) E('filters-selectAll-command').doCommand();"/>
|
||||
<key id="filters-copy-key" key="C" modifiers="accel" oncommand="if (FilterActions.focused) E('filters-copy-command').doCommand();"/>
|
||||
<key id="filters-cut-key" key="X" modifiers="accel" oncommand="if (FilterActions.focused) E('filters-cut-command').doCommand();"/>
|
||||
<key id="filters-paste-key" key="V" modifiers="accel" oncommand="if (FilterActions.focused) E('filters-paste-command').doCommand();"/>
|
||||
<key id="backup-key" key="E" modifiers="accel" oncommand="E('backup').doCommand();"/>
|
||||
<key id="restore-key" key="I" modifiers="accel" oncommand="E('restoreOwnBackup').doCommand();"/>
|
||||
<key id="find-key" key="F" modifiers="accel" oncommand="if (FilterActions.visible) E('find-command').doCommand();"/>
|
||||
<key id="find-again-key" key="G" modifiers="accel" oncommand="if (FilterActions.visible) E('find-again-command').doCommand();"/>
|
||||
<key id="find-previous-key" key="G" modifiers="accel,shift" oncommand="if (FilterActions.visible) E('find-previous-command').doCommand();"/>
|
||||
<key id="find-again-key2" keycode="VK_F3" oncommand="if (FilterActions.visible) E('find-again-command').doCommand();"/>
|
||||
<key id="find-previous-key2" keycode="VK_F3" modifiers="shift" oncommand="if (FilterActions.visible) E('find-previous-command').doCommand();"/>
|
||||
</keyset>
|
||||
|
||||
<commandset id="filtersCommandset">
|
||||
<command id="subscription-update-command" oncommand="SubscriptionActions.updateFilters();"/>
|
||||
<command id="subscription-update-all-command" oncommand="SubscriptionActions.updateAllFilters();"/>
|
||||
<command id="subscription-editTitle-command" oncommand="SubscriptionActions.editTitle();"/>
|
||||
<command id="subscription-delete-command" oncommand="SubscriptionActions.remove();"/>
|
||||
<command id="subscription-showHideFilters-command" oncommand="E('filtersGrippy').doCommand();"/>
|
||||
<command id="subscription-moveUp-command" oncommand="SubscriptionActions.moveUp();"/>
|
||||
<command id="subscription-moveDown-command" oncommand="SubscriptionActions.moveDown();"/>
|
||||
<command id="subscription-add-command" oncommand="SelectSubscription.start(event);"/>
|
||||
<command id="subscription-addSelected-command" oncommand="SelectSubscription.add();"/>
|
||||
<command id="subscription-addOther-command" oncommand="SelectSubscription.chooseOther();"/>
|
||||
<command id="group-add-command" oncommand="SubscriptionActions.addGroup();"/>
|
||||
<command id="filters-selectAll-command" oncommand="FilterActions.selectAll();"/>
|
||||
<command id="filters-edit-command" oncommand="FilterActions.startEditing();"/>
|
||||
<command id="filters-add-command" oncommand="FilterActions.insertFilter();"/>
|
||||
<command id="filters-delete-command" oncommand="FilterActions.deleteSelected();"/>
|
||||
<command id="filters-resetHitCounts-command" oncommand="FilterActions.resetHitCounts();"/>
|
||||
<command id="filters-moveUp-command" oncommand="FilterActions.moveUp();"/>
|
||||
<command id="filters-moveDown-command" oncommand="FilterActions.moveDown();"/>
|
||||
<command id="filters-copy-command" oncommand="FilterActions.copySelected(true);"/>
|
||||
<command id="filters-cut-command" oncommand="FilterActions.copySelected(false);"/>
|
||||
<command id="filters-paste-command" oncommand="FilterActions.paste();"/>
|
||||
<command id="find-command" oncommand="E('findbar').startFind(E('findbar').FIND_NORMAL)"/>
|
||||
<command id="find-again-command" oncommand="E('findbar').onFindAgainCommand(false)"/>
|
||||
<command id="find-previous-command" oncommand="E('findbar').onFindAgainCommand(true)"/>
|
||||
</commandset>
|
||||
|
||||
<popupset id="filtersPopupset">
|
||||
<menupopup id="filters-view-menu1" onpopupshowing="FilterActions.fillColumnPopup(this);">
|
||||
<menuitem id="filters-view-filter1" label="&filter.column;" accesskey="&filter.accesskey;" type="checkbox" disabled="true"/>
|
||||
<menuitem id="filters-view-slow1" label="&slow.column;" accesskey="&slow.accesskey;" type="checkbox" oncommand="FilterActions.toggleColumn('col-slow')"/>
|
||||
<menuitem id="filters-view-enabled1" label="&enabled.column;" accesskey="&enabled.accesskey;" type="checkbox" oncommand="FilterActions.toggleColumn('col-enabled')"/>
|
||||
<menuitem id="filters-view-hitcount1" label="&hitcount.column;" accesskey="&hitcount.accesskey;" type="checkbox" oncommand="FilterActions.toggleColumn('col-hitcount')"/>
|
||||
<menuitem id="filters-view-lasthit1" label="&lasthit.column;" accesskey="&lasthit.accesskey;" type="checkbox" oncommand="FilterActions.toggleColumn('col-lasthit')"/>
|
||||
<menuseparator/>
|
||||
<menu id="filters-sort-menu1" label="&sort.label;" accesskey="&sort.accesskey;">
|
||||
<menupopup id="filters-sort-popup1">
|
||||
<menuitem id="filters-sort-none1" label="&sort.none.label;" accesskey="&sort.none.accesskey;" type="radio" name="sortColumn" oncommand="FilterView.sortBy(null)"/>
|
||||
<menuitem id="filters-sort-filter1" label="&filter.column;" accesskey="&filter.accesskey;" type="radio" name="sortColumn" oncommand="FilterView.sortBy('col-filter')"/>
|
||||
<menuitem id="filters-sort-slow1" label="&slow.column;" accesskey="&slow.accesskey;" type="radio" name="sortColumn" oncommand="FilterView.sortBy('col-slow')"/>
|
||||
<menuitem id="filters-sort-enabled1" label="&enabled.column;" accesskey="&enabled.accesskey;" type="radio" name="sortColumn" oncommand="FilterView.sortBy('col-enabled')"/>
|
||||
<menuitem id="filters-sort-hitcount1" label="&hitcount.column;" accesskey="&hitcount.accesskey;" type="radio" name="sortColumn" oncommand="FilterView.sortBy('col-hitcount')"/>
|
||||
<menuitem id="filters-sort-lasthit1" label="&lasthit.column;" accesskey="&lasthit.accesskey;" type="radio" name="sortColumn" oncommand="FilterView.sortBy('col-lasthit')"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="filters-sort-asc1" label="&sort.ascending.label;" accesskey="&sort.ascending.accesskey;" type="radio" name="sortOrder" oncommand="FilterActions.setSortOrder('ascending')"/>
|
||||
<menuitem id="filters-sort-desc1" label="&sort.descending.label;" accesskey="&sort.descending.accesskey;" type="radio" name="sortOrder" oncommand="FilterActions.setSortOrder('descending')"/>
|
||||
</menupopup>
|
||||
</menu>
|
||||
</menupopup>
|
||||
<tooltip id="filtersTooltip" onpopupshowing="FilterActions.fillTooltip(event);">
|
||||
<grid>
|
||||
<columns>
|
||||
<column/>
|
||||
<column flex="1"/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row id="tooltip-filter-row" align="top">
|
||||
<label class="tooltipLabel" value="&filter.column;"/>
|
||||
<vbox id="tooltip-filter"/>
|
||||
</row>
|
||||
<row id="tooltip-hitcount-row">
|
||||
<label class="tooltipLabel" value="&hitcount.column;"/>
|
||||
<description id="tooltip-hitcount"/>
|
||||
</row>
|
||||
<row id="tooltip-lasthit-row">
|
||||
<label class="tooltipLabel" value="&lasthit.column;"/>
|
||||
<description id="tooltip-lasthit"/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
|
||||
<description id="tooltip-additional"/>
|
||||
</tooltip>
|
||||
</popupset>
|
||||
|
||||
<hbox id="content" flex="1">
|
||||
<tabbox id="tabs" flex="1" persist="selectedIndex">
|
||||
<tabs onselect="onTabChange(this.parentNode);">
|
||||
<tab label="&subscriptions.tab.label;"/>
|
||||
<tab label="&filters.tab.label;"/>
|
||||
</tabs>
|
||||
<tabpanels flex="1">
|
||||
<tabpanel id="subscriptionsTab" orient="vertical" flex="1">
|
||||
<hbox pack="end">
|
||||
<button id="selectSubscriptionButton" label="&addSubscription.label;…" accesskey="&addSubscription.accesskey;" command="subscription-add-command"/>
|
||||
</hbox>
|
||||
|
||||
<panel id="selectSubscriptionPanel" type="arrow" position="bottomcenter topleft"
|
||||
orient="vertical" onkeypress="SelectSubscription.keyPress(event);">
|
||||
<menuitem id="selectSubscriptionTemplate" hidden="true"
|
||||
class="{localePrefix ? 'localeMatch' : ''}"
|
||||
label="{node.getAttribute('title')}"
|
||||
value="{node.getAttribute('url')}">
|
||||
<label class="selectSubscriptionItem" value="{node.getAttribute('title')}"/>
|
||||
<label class="selectSubscriptionItem" value=" ("/>
|
||||
<label class="selectSubscriptionItem" value="{node.getAttribute('specialization')}"/>
|
||||
<label class="selectSubscriptionItem" value=")"/>
|
||||
</menuitem>
|
||||
<menulist id="selectSubscription">
|
||||
<menupopup/>
|
||||
</menulist>
|
||||
<hbox align="baseline">
|
||||
<label class="text-link" value="&addSubscriptionOther.label;" onclick="E('subscription-addOther-command').doCommand();"/>
|
||||
<spacer flex="1"/>
|
||||
<button id="selectSubscriptionAccept" default="true" label="&addSubscriptionAdd.label;" command="subscription-addSelected-command"/>
|
||||
<spacer flex="1"/>
|
||||
<button label="&addSubscriptionCancel.label;" oncommand="E('selectSubscriptionPanel').hidePopup();"/>
|
||||
</hbox>
|
||||
</panel>
|
||||
|
||||
<richlistitem id="subscriptionTemplate" class="subscription" hidden="true" orient="vertical"
|
||||
onmousedown="this._wasSelected = (this.parentNode.selectedItem == this);"
|
||||
ondragstart="SubscriptionActions.startDrag(event, this);"
|
||||
ondragend="SubscriptionActions.endDrag();"
|
||||
ondragover="SubscriptionActions.dragOver(event);"
|
||||
ondrop="SubscriptionActions.drop(event, this);"
|
||||
oncontextmenu="SubscriptionActions.openMenu(event, this);">
|
||||
<vbox class="{subscription.disabled ? 'disabled' : ''}">
|
||||
<hbox align="center">
|
||||
<checkbox label="&subscription.enabled.label;" class="enabledCheckbox tabable" tabindex="-1"
|
||||
checked="{subscription.disabled ? 'false' : 'true'}" oncommand="SubscriptionActions.setDisabled(this, !this.checked);"/>
|
||||
|
||||
<vbox flex="1">
|
||||
<hbox align="center">
|
||||
<deck class="titleBox" flex="1" selectedIndex="0" onselect="event.stopPropagation();">
|
||||
<description ondblclick="if (event.button == 0) TitleEditor.start(this, true);">
|
||||
<description class="title" value="{subscription.title}" flex="1" crop="end"/>
|
||||
<hbox align="baseline">
|
||||
<description value=" ("/>
|
||||
<if condition="{isExternal}">
|
||||
<description value="&subscription.external.label;"/>
|
||||
<else/>
|
||||
<if condition="{subscription.homepage}">
|
||||
<description class="link" value="&subscription.homepage.label;"
|
||||
_url="{subscription.homepage}" tooltiptext="{subscription.homepage}"
|
||||
onclick="if (event.button == 0) { event.stopPropagation();Utils.loadInBrowser(this.getAttribute('_url')); }"/>
|
||||
<description value=", "/>
|
||||
</if>
|
||||
<description class="link" value="&subscription.source.label;"
|
||||
_url="{subscription.url}" tooltiptext="{subscription.url}"
|
||||
onclick="if (event.button == 0) { event.stopPropagation();Utils.loadInBrowser(this.getAttribute('_url')); }"/>
|
||||
</if>
|
||||
<description value=")"/>
|
||||
</hbox>
|
||||
</description>
|
||||
<textbox oncontextmenu="event.stopPropagation();" class="titleEditor" onkeypress="TitleEditor.keyPress(event);" onblur="TitleEditor.end(true);"/>
|
||||
</deck>
|
||||
</hbox>
|
||||
<hbox align="center">
|
||||
<description flex="1" class="status">
|
||||
<description value="&subscription.lastDownload.label;"/>
|
||||
<description value=" "/>
|
||||
<if condition="{downloading}">
|
||||
<description value="&subscription.lastDownload.inProgress;"/>
|
||||
<elif condition="{!subscription.lastDownload}"/>
|
||||
<description value="&subscription.lastDownload.unknown;"/>
|
||||
<else/>
|
||||
<description value="{formatTime(subscription.lastDownload * 1000)}"/>
|
||||
<description value=" "/>
|
||||
<if condition="{subscription.downloadStatus}">
|
||||
<hbox>
|
||||
<description value=" ("/>
|
||||
<if condition="{subscription.downloadStatus == 'synchronize_invalid_url'}">
|
||||
<description value="&subscription.lastDownload.invalidURL;"/>
|
||||
<elif condition="{subscription.downloadStatus == 'synchronize_connection_error'}"/>
|
||||
<description value="&subscription.lastDownload.connectionError;"/>
|
||||
<elif condition="{subscription.downloadStatus == 'synchronize_invalid_data'}"/>
|
||||
<description value="&subscription.lastDownload.invalidData;"/>
|
||||
<elif condition="{subscription.downloadStatus == 'synchronize_checksum_mismatch'}"/>
|
||||
<description value="&subscription.lastDownload.checksumMismatch;"/>
|
||||
<else/> <!-- synchronize_ok -->
|
||||
<description value="&subscription.lastDownload.success;"/>
|
||||
</if>
|
||||
<description value=")"/>
|
||||
</hbox>
|
||||
</if>
|
||||
</if>
|
||||
</description>
|
||||
</hbox>
|
||||
</vbox>
|
||||
|
||||
<button class="actionButton tabable" type="menu" label="&subscription.actions.label;" tabindex="-1">
|
||||
<menupopup class="actionMenu">
|
||||
<menuitem label="&subscription.editTitle.label;" key="edit-key" command="subscription-editTitle-command"/>
|
||||
<menuitem label="&subscription.update.label;" key="subscription-update-key" command="subscription-update-command"/>
|
||||
<menuitem label="&subscription.showHideFilters.label;" key="subscription-showHideFilters-key" command="subscription-showHideFilters-command"/>
|
||||
<menuitem label="&subscription.delete.label;…" key="delete-key" command="subscription-delete-command"/>
|
||||
<menuseparator/>
|
||||
<menuitem label="&subscription.moveUp.label;" key="moveUp-key" command="subscription-moveUp-command"/>
|
||||
<menuitem label="&subscription.moveDown.label;" key="moveDown-key" command="subscription-moveDown-command"/>
|
||||
</menupopup>
|
||||
</button>
|
||||
</hbox>
|
||||
|
||||
<description class="warning" hidden="{!subscription.upgradeRequired}">&subscription.minVersion.warning;</description>
|
||||
<description class="warning" hidden="{!disabledFilters}">
|
||||
&subscription.disabledFilters.warning;
|
||||
<description class="link" value="&subscription.disabledFilters.enable;" onclick="SubscriptionActions.enableFilters(this);"/>
|
||||
</description>
|
||||
</vbox>
|
||||
</richlistitem>
|
||||
|
||||
<deck id="noSubscriptionsDeck" flex="1">
|
||||
<description flex="1">&noSubscriptions.text;</description>
|
||||
<richlistbox id="subscriptions" class="initialFocus" flex="1"
|
||||
onselect="onSelectionChange(this);"
|
||||
ondragover="SubscriptionActions.dragOver(event);"
|
||||
ondrop="SubscriptionActions.drop(event, null);"
|
||||
onkeypress="SubscriptionActions.keyPress(event);">
|
||||
</richlistbox>
|
||||
</deck>
|
||||
</tabpanel>
|
||||
<tabpanel id="filtersTab" orient="vertical" flex="1">
|
||||
<hbox pack="end">
|
||||
<button id="addGroupButton" label="&addGroup.label;" accesskey="&addGroup.accesskey;" command="group-add-command"/>
|
||||
</hbox>
|
||||
|
||||
<richlistitem id="groupTemplate" class="subscription" hidden="true" orient="vertical"
|
||||
onmousedown="this._wasSelected = (this.parentNode.selectedItem == this);"
|
||||
ondragstart="SubscriptionActions.startDrag(event, this);"
|
||||
ondragend="SubscriptionActions.endDrag();"
|
||||
ondragover="SubscriptionActions.dragOver(event);"
|
||||
ondrop="SubscriptionActions.drop(event, this);"
|
||||
oncontextmenu="SubscriptionActions.openMenu(event, this);">
|
||||
<hbox class="{subscription.disabled ? 'disabled' : ''}" align="center">
|
||||
<checkbox label="&subscription.enabled.label;" class="enabledCheckbox tabable" tabindex="-1"
|
||||
checked="{subscription.disabled ? 'false' : 'true'}" oncommand="SubscriptionActions.setDisabled(this, !this.checked);"/>
|
||||
<hbox align="center" flex="1">
|
||||
<deck class="titleBox" flex="1" selectedIndex="0" onselect="event.stopPropagation();">
|
||||
<description class="title" value="{subscription.title}" crop="end" ondblclick="if (event.button == 0) TitleEditor.start(this, true);"/>
|
||||
<textbox oncontextmenu="event.stopPropagation();" class="titleEditor" onkeypress="TitleEditor.keyPress(event);" onblur="TitleEditor.end(true);"/>
|
||||
</deck>
|
||||
</hbox>
|
||||
<button class="actionButton tabable" type="menu" label="&subscription.actions.label;" tabindex="-1">
|
||||
<menupopup class="actionMenu">
|
||||
<menuitem label="&subscription.editTitle.label;" key="edit-key" command="subscription-editTitle-command"/>
|
||||
<menuitem label="&subscription.showHideFilters.label;" key="subscription-showHideFilters-key" command="subscription-showHideFilters-command"/>
|
||||
<menuitem label="&subscription.delete.label;…" key="delete-key" command="subscription-delete-command"/>
|
||||
<menuseparator/>
|
||||
<menuitem label="&subscription.moveUp.label;" key="moveUp-key" command="subscription-moveUp-command"/>
|
||||
<menuitem label="&subscription.moveDown.label;" key="moveDown-key" command="subscription-moveDown-command"/>
|
||||
</menupopup>
|
||||
</button>
|
||||
</hbox>
|
||||
</richlistitem>
|
||||
|
||||
<deck id="noFiltersDeck" flex="1">
|
||||
<description flex="1">&noFilters.text;</description>
|
||||
<richlistbox id="groups" class="initialFocus" flex="1"
|
||||
onselect="onSelectionChange(this);"
|
||||
ondragover="SubscriptionActions.dragOver(event);"
|
||||
ondrop="SubscriptionActions.drop(event, null);"
|
||||
onkeypress="SubscriptionActions.keyPress(event);">
|
||||
</richlistbox>
|
||||
</deck>
|
||||
</tabpanel>
|
||||
</tabpanels>
|
||||
</tabbox>
|
||||
|
||||
<splitter id="filtersSplitter" persist="state" orient="horizontal" collapse="after" state="collapsed" oncommand="FilterView.refresh();onSplitterStateChange(this);">
|
||||
<grippy id="filtersGrippy"/>
|
||||
</splitter>
|
||||
|
||||
<vbox id="filtersContainer" persist="width height" width="500">
|
||||
<hbox pack="end">
|
||||
<button id="findButton" label="&find.label;" accesskey="&find.accesskey;" command="find-command"/>
|
||||
<button id="filterActionButton" type="menu" label="&filter.actions.label;">
|
||||
<menupopup id="filterActionMenu" onpopupshowing="FilterActions.fillActionsPopup();">
|
||||
<menuitem label="&filter.edit.label;" key="edit-key" command="filters-edit-command"/>
|
||||
<menuitem label="&filter.cut.label;" key="filters-cut-key" command="filters-cut-command"/>
|
||||
<menuitem label="&filter.copy.label;" key="filters-copy-key" command="filters-copy-command"/>
|
||||
<menuitem label="&filter.paste.label;" key="filters-paste-key" command="filters-paste-command"/>
|
||||
<menuitem label="&filter.delete.label;" key="delete-key" command="filters-delete-command"/>
|
||||
<menuseparator/>
|
||||
<menuitem label="&filter.selectAll.label;" key="filters-selectAll-key" command="filters-selectAll-command"/>
|
||||
<menuitem label="&filter.resetHitCounts.label;" command="filters-resetHitCounts-command"/>
|
||||
<menuseparator/>
|
||||
<menuitem label="&filter.moveUp.label;" key="moveUp-key" command="filters-moveUp-command"/>
|
||||
<menuitem label="&filter.moveDown.label;" key="moveDown-key" command="filters-moveDown-command"/>
|
||||
<menuseparator/>
|
||||
<menu id="viewMenu" label="&viewMenu.label;"/>
|
||||
</menupopup>
|
||||
</button>
|
||||
<button id="addFilterButton" label="&addFilter.label;" accesskey="&addFilter.accesskey;" command="filters-add-command"/>
|
||||
</hbox>
|
||||
<tree id="filtersTree"
|
||||
flex="1"
|
||||
editable="true"
|
||||
seltype="multiple"
|
||||
enableColumnDrag="true"
|
||||
hidecolumnpicker="true"
|
||||
_removewarning="&filters.remove.warning;">
|
||||
<treecols context="filters-view-menu1">
|
||||
<treecol id="col-enabled" label="&enabled.column;" cycler="true" flex="0" persist="width ordinal sortDirection hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="col-filter" label="&filter.column;" flex="10" persist="width ordinal sortDirection hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="col-slow" label="!" display="&slow.column;" tooltiptext="&slow.column;" flex="0" width="16" persist="width ordinal sortDirection hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="col-hitcount" label="&hitcount.column;" flex="0" persist="width ordinal sortDirection hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="col-lasthit" label="&lasthit.column;" hidden="true" flex="4" persist="width ordinal sortDirection hidden"/>
|
||||
</treecols>
|
||||
|
||||
<treechildren id="filtersTreeChildren"
|
||||
oncontextmenu="E('filterActionMenu').openPopupAtScreen(event.screenX, event.screenY, true);"
|
||||
tooltip="filtersTooltip"
|
||||
noGroupText="&noGroupSelected.text;"
|
||||
noFiltersText="&noFiltersInGroup.text;"
|
||||
ondragstart="FilterActions.startDrag(event);"
|
||||
ondragend="FilterActions.endDrag(event);"/>
|
||||
</tree>
|
||||
</vbox>
|
||||
</hbox>
|
||||
|
||||
<findbar id="findbar"/>
|
||||
|
||||
<hbox id="buttons">
|
||||
<button id="backupButton" type="menu"
|
||||
label="&backupButton.label;" accesskey="&backupButton.accesskey;"
|
||||
_backupDialogTitle="&backup.label;" _restoreDialogTitle="&restore.own.label;"
|
||||
_fileFilterComplete="&backup.complete.title;" _fileFilterCustom="&backup.custom.title;"
|
||||
_backupError="&backup.error;" _restoreError="&restore.error;"
|
||||
_restoreCompleteWarning="&restore.complete.warning;" _restoreCustomWarning="&restore.custom.warning;"
|
||||
_restoreVersionWarning="&restore.minVersion.warning;"
|
||||
oncommand="if (event.target == this) Utils.runAsync(function() this.open = true, this);">
|
||||
<menupopup onpopupshowing="Backup.fillRestorePopup();">
|
||||
<menuitem id="backup" key="backup-key" label="&backup.label;…" oncommand="Backup.backupToFile();"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="restoreBackupTemplate" label="&restore.default.label;" hidden="true"/>
|
||||
<menuitem id="restoreOwnBackup" key="restore-key" label="&restore.own.label;…" oncommand="Backup.restoreFromFile();"/>
|
||||
</menupopup>
|
||||
</button>
|
||||
|
||||
<spacer flex="1"/>
|
||||
|
||||
<button id="close" dlgtype="accept" label="&close.label;"/>
|
||||
</hbox>
|
||||
|
||||
</dialog>
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
function init()
|
||||
{
|
||||
|
||||
if (Utils.isFennec)
|
||||
{
|
||||
let topWnd = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
if (topWnd.wrappedJSObject)
|
||||
topWnd = topWnd.wrappedJSObject;
|
||||
|
||||
// window.close() closes the entire window (bug 642604), make sure to close
|
||||
// only a single tab instead.
|
||||
if ("BrowserUI" in topWnd)
|
||||
{
|
||||
window.close = function()
|
||||
{
|
||||
topWnd.BrowserUI.closeTab();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
generateLinkText(E("changeDescription"));
|
||||
|
||||
for each (let subscription in FilterStorage.subscriptions)
|
||||
{
|
||||
if (subscription instanceof DownloadableSubscription && subscription.url)
|
||||
{
|
||||
E("listName").textContent = subscription.title;
|
||||
|
||||
let link = E("listHomepage");
|
||||
link.setAttribute("_url", subscription.homepage);
|
||||
link.setAttribute("tooltiptext", subscription.homepage);
|
||||
|
||||
E("listNameContainer").hidden = false;
|
||||
E("listNone").hidden = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateLinkText(element)
|
||||
{
|
||||
let template = element.getAttribute("_textTemplate");
|
||||
|
||||
let [, beforeLink, linkText, afterLink] = /(.*)\[link\](.*)\[\/link\](.*)/.exec(template) || [null, "", template, ""];
|
||||
while (element.firstChild && element.firstChild.nodeType != Node.ELEMENT_NODE)
|
||||
element.removeChild(element.firstChild);
|
||||
while (element.lastChild && element.lastChild.nodeType != Node.ELEMENT_NODE)
|
||||
element.removeChild(element.lastChild);
|
||||
if (!element.firstChild)
|
||||
return;
|
||||
|
||||
element.firstChild.textContent = linkText;
|
||||
element.insertBefore(document.createTextNode(beforeLink), element.firstChild);
|
||||
element.appendChild(document.createTextNode(afterLink));
|
||||
}
|
||||
|
||||
function openFilters()
|
||||
{
|
||||
if (Utils.isFennec)
|
||||
{
|
||||
let topWnd = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
if (topWnd.wrappedJSObject)
|
||||
topWnd = topWnd.wrappedJSObject;
|
||||
|
||||
// window.close() closes the entire window (bug 642604), make sure to close
|
||||
// only a single tab instead.
|
||||
if ("BrowserUI" in topWnd)
|
||||
{
|
||||
topWnd.BrowserUI.showPanel("addons-container");
|
||||
function showOptions()
|
||||
{
|
||||
if (!topWnd.ExtensionsView.getElementForAddon(Utils.addonID))
|
||||
Utils.runAsync(showOptions);
|
||||
else
|
||||
topWnd.ExtensionsView.showOptions(Utils.addonID);
|
||||
}
|
||||
showOptions();
|
||||
}
|
||||
}
|
||||
else
|
||||
Utils.openFiltersDialog();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://@ADDON_CHROME_NAME@/skin/firstRun.css" type="text/css"?>
|
||||
|
||||
<!DOCTYPE dialog SYSTEM "chrome://@ADDON_CHROME_NAME@/locale/firstRun.dtd">
|
||||
|
||||
<dialog
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
buttons="accept"
|
||||
title="&dialog.title;"
|
||||
id="abpFirstRun"
|
||||
windowtype="abp:firstRun"
|
||||
onload="init()">
|
||||
|
||||
<script type="application/x-javascript;version=1.7" src="utils.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="firstRun.js"/>
|
||||
|
||||
<description>&confirmation;</description>
|
||||
|
||||
<vbox class="sectionContainer">
|
||||
<description class="sectionTitle">&advancedSection;</description>
|
||||
|
||||
<description>&listSelection1;</description>
|
||||
|
||||
<vbox id="listNameContainer" hidden="true">
|
||||
<description id="listName"/>
|
||||
<label class="text-link" id="listHomepage" value="&visitHomepage.label;" onclick="Utils.loadInBrowser(this.getAttribute('_url'))"/>
|
||||
</vbox>
|
||||
<description id="listNone">&noList;</description>
|
||||
</vbox>
|
||||
|
||||
<description id="changeDescription" _textTemplate="&listSelection2;">
|
||||
<label class="text-link" onclick="openFilters();"/>
|
||||
</description>
|
||||
|
||||
</dialog>
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Draws a blinking border for a list of matching nodes.
|
||||
*/
|
||||
|
||||
var flasher = {
|
||||
nodes: null,
|
||||
count: 0,
|
||||
timer: null,
|
||||
|
||||
flash: function(nodes)
|
||||
{
|
||||
this.stop();
|
||||
if (nodes)
|
||||
nodes = nodes.filter(function(node) node.nodeType == Node.ELEMENT_NODE);
|
||||
if (!nodes || !nodes.length)
|
||||
return;
|
||||
|
||||
if (Prefs.flash_scrolltoitem && nodes[0].ownerDocument)
|
||||
{
|
||||
// Ensure that at least one node is visible when flashing
|
||||
let wnd = nodes[0].ownerDocument.defaultView;
|
||||
try
|
||||
{
|
||||
let hooks = wnd.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow)
|
||||
.document.getElementById("abp-hooks");
|
||||
if (hooks.wrappedJSObject)
|
||||
hooks = hooks.wrappedJSObject;
|
||||
|
||||
let viewer = hooks.getBrowser().markupDocumentViewer;
|
||||
viewer.scrollToNode(nodes[0]);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
this.nodes = nodes;
|
||||
this.count = 0;
|
||||
|
||||
this.doFlash();
|
||||
},
|
||||
|
||||
doFlash: function() {
|
||||
if (this.count >= 12) {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.count % 2)
|
||||
this.switchOff();
|
||||
else
|
||||
this.switchOn();
|
||||
|
||||
this.count++;
|
||||
|
||||
this.timer = window.setTimeout(function() {flasher.doFlash()}, 300);
|
||||
},
|
||||
|
||||
stop: function() {
|
||||
if (this.timer) {
|
||||
window.clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
if (this.nodes) {
|
||||
this.switchOff();
|
||||
this.nodes = null;
|
||||
}
|
||||
},
|
||||
|
||||
setOutline: function(outline, offset)
|
||||
{
|
||||
for (var i = 0; i < this.nodes.length; i++)
|
||||
{
|
||||
if ("style" in this.nodes[i])
|
||||
{
|
||||
this.nodes[i].style.outline = outline;
|
||||
this.nodes[i].style.outlineOffset = offset;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
switchOn: function()
|
||||
{
|
||||
this.setOutline("#CC0000 dotted 2px", "-2px");
|
||||
},
|
||||
|
||||
switchOff: function()
|
||||
{
|
||||
this.setOutline("", "");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#filter substitution
|
||||
|
||||
[.] chrome.jar:
|
||||
% content @ADDON_CHROME_NAME@ chrome/content/
|
||||
* content/about.js
|
||||
* content/about.xul
|
||||
content/composer.js
|
||||
* content/composer.xul
|
||||
content/errors.html
|
||||
content/filters-backup.js
|
||||
content/filters-filteractions.js
|
||||
content/filters-filterview.js
|
||||
content/filters-search.js
|
||||
content/filters-subscriptionactions.js
|
||||
content/filters-subscriptionview.js
|
||||
content/filters.js
|
||||
* content/filters.xul
|
||||
content/firstRun.js
|
||||
* content/firstRun.xul
|
||||
content/flasher.js
|
||||
* content/mailOverlay.xul
|
||||
* content/navigatorOverlay.xul
|
||||
content/objtabs.css
|
||||
* content/overlay.js
|
||||
* content/overlayGeneral.xul
|
||||
* content/phoenixOverlay.xul
|
||||
content/progressBar.xml
|
||||
content/sendReport.js
|
||||
* content/sendReport.xul
|
||||
* content/settings.xul
|
||||
* content/sidebar.js
|
||||
* content/sidebar.xul
|
||||
* content/sidebarDetached.xul
|
||||
content/subscriptions.xml
|
||||
content/subscriptionSelection.js
|
||||
* content/subscriptionSelection.xul
|
||||
* content/utils.js
|
||||
|
||||
#ifdef ADDON_TARGET_BASILISK
|
||||
% overlay chrome://browser/content/browser.xul chrome://@ADDON_CHROME_NAME@/content/phoenixOverlay.xul application=@ADDON_TARGET_APP_ID@ application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
|
||||
#else
|
||||
% overlay chrome://browser/content/browser.xul chrome://@ADDON_CHROME_NAME@/content/phoenixOverlay.xul application=@ADDON_TARGET_APP_ID@
|
||||
#endif
|
||||
|
||||
% overlay chrome://navigator/content/navigator.xul chrome://@ADDON_CHROME_NAME@/content/navigatorOverlay.xul application={a3210b97-8e8a-4737-9aa0-aa0e607640b9}
|
||||
% overlay chrome://navigator/content/navigator.xul chrome://@ADDON_CHROME_NAME@/content/mailOverlay.xul application={3550f703-e582-4d05-9a08-453d09bdfdc6}
|
||||
|
||||
# Hack to prevent .Net Framework Assistant from messing up the browser
|
||||
% override chrome://dotnetassistant/content/bootstrap.xul data:text/xml,<nada/>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<?xul-overlay href="chrome://adblockplus/content/ui/overlayGeneral.xul"?>
|
||||
|
||||
<overlay id="abp-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<!-- Window extensions -->
|
||||
<window id="messengerWindow">
|
||||
<popupset id="abp-popupset"/>
|
||||
<keyset id="abp-keyset"/>
|
||||
<commandset id="abp-commandset"/>
|
||||
<box id="abp-hooks" getBrowser="return this.window.getMessageBrowser();"
|
||||
addTab="this.window.openNewTabWith(arguments[0], (arguments[1] ? this.window.content.document : null), (arguments[1] ? arguments[1].shiftKey : false));"
|
||||
getContextMenu="return this.E('messagePaneContext');"
|
||||
getToolbox="return this.E('mail-toolbox')"
|
||||
getDefaultToolbar="return this.E('msgToolbar');" toolbarInsertBefore="return this.E('button-junk');"/>
|
||||
</window>
|
||||
|
||||
<!-- Status bar -->
|
||||
<statusbar id="status-bar">
|
||||
<statusbarpanel id="abp-status"/>
|
||||
</statusbar>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<toolbarpalette id="MailToolbarPalette">
|
||||
<toolbarbutton id="abp-toolbarbutton" type="menu-button" insertafter="button-junk"
|
||||
class="toolbarbutton-1"/>
|
||||
</toolbarpalette>
|
||||
|
||||
<!-- Tools menu -->
|
||||
<menupopup id="taskPopup">
|
||||
<menu id="abp-menuitem" insertafter="downloadmgr,javaScriptConsole"/>
|
||||
</menupopup>
|
||||
|
||||
<!-- Context menu -->
|
||||
<menupopup id="messagePaneContext">
|
||||
<menuitem id="abp-image-menuitem"/>
|
||||
<menuitem id="abp-object-menuitem"/>
|
||||
<menuitem id="abp-media-menuitem"/>
|
||||
<menuitem id="abp-frame-menuitem"/>
|
||||
<menuitem id="abp-removeWhitelist-menuitem"/>
|
||||
</menupopup>
|
||||
|
||||
<!-- Fake sidebar -->
|
||||
<vbox id="messagepanebox">
|
||||
<splitter id="abp-sidebar-splitter"/>
|
||||
<vbox id="abp-sidebar"/>
|
||||
</vbox>
|
||||
</overlay>
|
||||
@@ -3,5 +3,4 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<?xul-overlay href="chrome://@ADDON_CHROME_NAME@/content/overlayGeneral.xul"?>
|
||||
|
||||
<overlay id="abp-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<!-- Window extensions -->
|
||||
<window id="main-window">
|
||||
<popupset id="abp-popupset"/>
|
||||
<keyset id="abp-keyset"/>
|
||||
<commandset id="abp-commandset"/>
|
||||
<box id="abp-hooks"
|
||||
getBrowser="return this.window.gBrowser;"
|
||||
addTab="
|
||||
if (arguments[1] && 'openNewTabWith' in this.window)
|
||||
this.window.openNewTabWith(arguments[0], this.window.content.document, arguments[1].shiftKey);
|
||||
else
|
||||
this.window.gBrowser.loadOneTab(arguments[0], {inBackground: false});"
|
||||
getContextMenu="return this.E('contentAreaContextMenu');"
|
||||
getToolbox="return this.E('navigator-toolbox')"
|
||||
getDefaultToolbar="return this.E('nav-bar');" toolbarInsertBefore="return this.E('throbber-box');"/>
|
||||
</window>
|
||||
|
||||
<!-- Status bar -->
|
||||
<statusbar id="status-bar">
|
||||
<statusbarpanel id="abp-status" mousethrough="never" insertbefore="resizerBottomRight"/>
|
||||
</statusbar>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<toolbarpalette id="BrowserToolbarPalette">
|
||||
<toolbarbutton id="abp-toolbarbutton" type="menu-button" pack="end"
|
||||
class="toolbarbutton-1"/>
|
||||
</toolbarpalette>
|
||||
|
||||
<!-- Tools menu -->
|
||||
<menupopup id="taskPopup">
|
||||
<menu id="abp-menuitem" insertbefore="navBeginGlobalItems"/>
|
||||
</menupopup>
|
||||
|
||||
<!-- Context menu -->
|
||||
<menupopup id="contentAreaContextMenu">
|
||||
<menuitem id="abp-image-menuitem"/>
|
||||
<menuitem id="abp-object-menuitem"/>
|
||||
<menuitem id="abp-media-menuitem"/>
|
||||
<menuitem id="abp-frame-menuitem"/>
|
||||
<menuitem id="abp-removeWhitelist-menuitem"/>
|
||||
</menupopup>
|
||||
|
||||
<!-- Fake sidebar -->
|
||||
<vbox id="appcontent">
|
||||
<splitter id="abp-sidebar-splitter"/>
|
||||
<vbox id="abp-sidebar"/>
|
||||
</vbox>
|
||||
</overlay>
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@namespace url("http://www.w3.org/1999/xhtml");
|
||||
|
||||
.%%CLASSVISIBLETOP%%, .%%CLASSVISIBLEBOTTOM%%, .%%CLASSHIDDEN%%
|
||||
{
|
||||
position: fixed !important;
|
||||
display: block !important;
|
||||
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
z-index: 65535 !important;
|
||||
float: left !important;
|
||||
border-color: black !important;
|
||||
border-style: solid !important;
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
cursor: pointer !important;
|
||||
white-space: nowrap !important;
|
||||
font-family: Arial,Helvetica,Sans-Serif !important;
|
||||
font-size: 10px !important;
|
||||
font-style: normal !important;
|
||||
font-variant: normal !important;
|
||||
font-weight: normal !important;
|
||||
letter-spacing: normal !important;
|
||||
line-height: normal !important;
|
||||
text-align: center !important;
|
||||
text-decoration: none !important;
|
||||
text-indent: 0px !important;
|
||||
text-transform: none !important;
|
||||
direction: ltr !important;
|
||||
padding: 0px 5px !important;
|
||||
-moz-binding: none !important;
|
||||
-moz-user-focus: none !important;
|
||||
-moz-user-input: none !important;
|
||||
-moz-user-select: none !important;
|
||||
}
|
||||
|
||||
.%%CLASSVISIBLETOP%%, .%%CLASSHIDDEN%%
|
||||
{
|
||||
border-width: 1px 1px 0px 1px !important;
|
||||
border-top-left-radius: 10px !important;
|
||||
border-top-right-radius: 10px !important;
|
||||
border-bottom-left-radius: 0px !important;
|
||||
border-bottom-right-radius: 0px !important;
|
||||
}
|
||||
|
||||
.%%CLASSVISIBLEBOTTOM%%
|
||||
{
|
||||
border-width: 0px 1px 1px 1px !important;
|
||||
border-top-left-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
border-bottom-left-radius: 10px !important;
|
||||
border-bottom-right-radius: 10px !important;
|
||||
}
|
||||
|
||||
.%%CLASSVISIBLETOP%%, .%%CLASSVISIBLEBOTTOM%%
|
||||
{
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.%%CLASSHIDDEN%%
|
||||
{
|
||||
visibility: hidden !important;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
{
|
||||
let Cc = Components.classes;
|
||||
let Ci = Components.interfaces;
|
||||
let Cr = Components.results;
|
||||
let Cu = Components.utils;
|
||||
|
||||
// Use UIReady event to initialize in Fennec (bug 531071)
|
||||
let eventName = Cu.import("resource://@ADDON_CHROME_NAME@/modules/Utils.jsm", null).Utils.isFennec ? "UIReady" : "load";
|
||||
|
||||
window.addEventListener(eventName, function()
|
||||
{
|
||||
window.removeEventListener(eventName, arguments.callee, false);
|
||||
|
||||
if (!("@adblockplus.org/abp/public;1" in Cc))
|
||||
{
|
||||
// Force initialization (in Fennec we won't be initialized at this point)
|
||||
Cu.import("resource://@ADDON_CHROME_NAME@/modules/Bootstrap.jsm", null).Bootstrap.startup();
|
||||
}
|
||||
|
||||
Cu.import("resource://@ADDON_CHROME_NAME@/modules/AppIntegration.jsm", null).AppIntegration.addWindow(window);
|
||||
}, false);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<?xml-stylesheet href="chrome://@ADDON_CHROME_NAME@/skin/overlay.css" type="text/css"?>
|
||||
|
||||
<!DOCTYPE overlay SYSTEM "chrome://@ADDON_CHROME_NAME@/locale/overlay.dtd">
|
||||
|
||||
<overlay id="abp-overlay-general" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<script type="application/x-javascript;version=1.7" src="overlay.js"/>
|
||||
|
||||
<popupset id="abp-popupset">
|
||||
<!-- Icon's tooltip -->
|
||||
<tooltip id="abp-tooltip" orient="vertical">
|
||||
<description id="abp-tooltip-action" hidden="true"/>
|
||||
<label id="abp-tooltip-status-label" value="&status.tooltip;"/>
|
||||
<description id="abp-tooltip-status"/>
|
||||
<label id="abp-tooltip-blocked-label" value="&blocked.tooltip;" hidden="true"/>
|
||||
<description id="abp-tooltip-blocked" hidden="true"/>
|
||||
<label id="abp-tooltip-filters-label" value="&filters.tooltip;" hidden="true"/>
|
||||
<vbox id="abp-tooltip-filters" hidden="true"/>
|
||||
<description id="abp-tooltip-more-filters" value="…" hidden="true"/>
|
||||
</tooltip>
|
||||
|
||||
<!-- Icon's context menu -->
|
||||
<menupopup id="abp-status-popup" context="">
|
||||
<menuitem id="abp-status-opensidebar" label="&opensidebar.label;" accesskey="&opensidebar.accesskey;" key="abp-key-sidebar" command="abp-command-sidebar"/>
|
||||
<menuitem id="abp-status-closesidebar" label="&closesidebar.label;" accesskey="&closesidebar.accesskey;" key="abp-key-sidebar" command="abp-command-sidebar"/>
|
||||
<menuitem id="abp-status-filters" label="&filters.label;…" accesskey="&filters.accesskey;" key="abp-key-filters" command="abp-command-filters"/>
|
||||
<menuseparator id="abp-status-whitelist-sep"/>
|
||||
<menuitem id="abp-status-whitelistsite" labeltempl="&whitelist.site.label;" type="checkbox" command="abp-command-togglesitewhitelist"/>
|
||||
<menuitem id="abp-status-whitelistpage" label="&whitelist.page.label;" type="checkbox" command="abp-command-togglepagewhitelist"/>
|
||||
<menuitem id="abp-status-disabled" label="&disable.label;" type="checkbox" key="abp-key-enable" command="abp-command-enable"/>
|
||||
<menuseparator/>
|
||||
<menu id="abp-status-options" label="&options.label;" accesskey="&options.accesskey;">
|
||||
<menupopup id="abp-status-options-popup">
|
||||
<menuitem id="abp-status-frameobjects" label="&objecttabs.label;" accesskey="&objecttabs.accesskey;" type="checkbox" command="abp-command-toggleobjtabs"/>
|
||||
<menuitem id="abp-status-slowcollapse" label="&hideplaceholders.label;" accesskey="&hideplaceholders.accesskey;" type="checkbox" command="abp-command-togglecollapse"/>
|
||||
<menuitem id="abp-status-savestats" label="&counthits.label;" accesskey="&counthits.accesskey;" type="checkbox" command="abp-command-togglesavestats"/>
|
||||
<menuseparator id="abp-status-iconSettingsSeparator"/>
|
||||
<menuitem id="abp-status-showintoolbar" label="&showintoolbar.label;" accesskey="&showintoolbar.accesskey;" type="checkbox" command="abp-command-toggleshowintoolbar"/>
|
||||
<menuitem id="abp-status-showinstatusbar" label="&showinstatusbar.label;" accesskey="&showinstatusbar.accesskey;" type="checkbox" command="abp-command-toggleshowinstatusbar"/>
|
||||
</menupopup>
|
||||
</menu>
|
||||
</menupopup>
|
||||
</popupset>
|
||||
<keyset id="abp-keyset"/>
|
||||
<commandset id="abp-commandset">
|
||||
<!-- Dummy oncommand attributes are work-arounds for bug 371900 -->
|
||||
<command id="abp-command-filters" oncommand="//"/>
|
||||
<command id="abp-command-settings" oncommand="//"/>
|
||||
<command id="abp-command-sidebar" oncommand="//"/>
|
||||
<command id="abp-command-togglesitewhitelist"/>
|
||||
<command id="abp-command-togglepagewhitelist"/>
|
||||
<command id="abp-command-toggleobjtabs"/>
|
||||
<command id="abp-command-togglecollapse"/>
|
||||
<command id="abp-command-togglesavestats"/>
|
||||
<command id="abp-command-togglesync"/>
|
||||
<command id="abp-command-toggleshowintoolbar"/>
|
||||
<command id="abp-command-toggleshowinstatusbar"/>
|
||||
<command id="abp-command-enable" oncommand="//"/>
|
||||
</commandset>
|
||||
|
||||
<statusbarpanel id="abp-status" class="statusbarpanel-iconic"
|
||||
context="abp-status-popup" tooltip="abp-tooltip"/>
|
||||
|
||||
<toolbarbutton id="abp-toolbarbutton" label="&toolbarbutton.label;"
|
||||
tooltip="abp-tooltip" context="abp-status-popup"/>
|
||||
|
||||
<box id="abp-hooks" objtabtext="&objecttab.title;…" objtabtooltip="&objecttab.tooltip;"/>
|
||||
|
||||
<!-- Tools menu -->
|
||||
<menu id="abp-menuitem" label="&toolsmenu.label;"/>
|
||||
|
||||
<!-- Context menu -->
|
||||
<menuitem id="abp-image-menuitem" label="&context.image.label;…" hidden="true"/>
|
||||
<menuitem id="abp-object-menuitem" label="&context.object.label;…" hidden="true"/>
|
||||
<menuitem id="abp-media-menuitem" label="&context.media.label;…" hidden="true"/>
|
||||
<menuitem id="abp-frame-menuitem" label="&context.frame.label;…" hidden="true"/>
|
||||
<menuitem id="abp-removeWhitelist-menuitem" label="&context.removeWhitelist.label;" hidden="true"/>
|
||||
|
||||
<!-- Fake sidebar -->
|
||||
<splitter id="abp-sidebar-splitter" hidden="true"/>
|
||||
<vbox id="abp-sidebar" height="200" width="200" hidden="true" persist="height width">
|
||||
<toolbox id="abp-sidebar-header">
|
||||
<toolbar id="abp-sidebar-toolbar" align="center" grippyhidden="true" fullscreentoolbar="true">
|
||||
<label id="abp-sidebar-title" control="abp-sidebar-browser" value="&sidebar.title;" flex="1" crop="end"/>
|
||||
<toolbarbutton id="abp-sidebar-close" command="abp-command-sidebar" tooltiptext="&closesidebar.label;"/>
|
||||
</toolbar>
|
||||
</toolbox>
|
||||
<iframe id="abp-sidebar-browser" flex="1"/>
|
||||
</vbox>
|
||||
</overlay>
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<?xul-overlay href="chrome://@ADDON_CHROME_NAME@/content/overlayGeneral.xul"?>
|
||||
|
||||
<overlay id="abp-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<!-- Window extensions -->
|
||||
<window id="main-window">
|
||||
<popupset id="abp-popupset"/>
|
||||
<keyset id="abp-keyset"/>
|
||||
<commandset id="abp-commandset"/>
|
||||
<box id="abp-hooks"
|
||||
getBrowser="return this.window.gBrowser;"
|
||||
addTab="this.window.gBrowser.loadOneTab(arguments[0], null, null, null, false);"
|
||||
getContextMenu="return this.E('contentAreaContextMenu');"
|
||||
getToolbox="return this.E('navigator-toolbox')"
|
||||
getDefaultToolbar="return this.E('nav-bar');"/>
|
||||
</window>
|
||||
|
||||
<!-- Status bar -->
|
||||
<statusbar id="status-bar">
|
||||
<statusbarpanel id="abp-status" mousethrough="never" insertbefore="resizerBottomRight"/>
|
||||
</statusbar>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<toolbarpalette id="BrowserToolbarPalette">
|
||||
<toolbarbutton id="abp-toolbarbutton" type="menu-button" insertbefore="print-button"
|
||||
class="toolbarbutton-1"/>
|
||||
</toolbarpalette>
|
||||
|
||||
<!-- Tools menu -->
|
||||
<menupopup id="menu_ToolsPopup">
|
||||
<menu id="abp-menuitem" insertbefore="webDeveloperMenu" />
|
||||
</menupopup>
|
||||
|
||||
<!-- Context menu -->
|
||||
<menupopup id="contentAreaContextMenu">
|
||||
<menuitem id="abp-image-menuitem"/>
|
||||
<menuitem id="abp-object-menuitem"/>
|
||||
<menuitem id="abp-media-menuitem"/>
|
||||
<menuitem id="abp-frame-menuitem"/>
|
||||
<menuitem id="abp-removeWhitelist-menuitem"/>
|
||||
</menupopup>
|
||||
|
||||
<!-- Fake sidebar -->
|
||||
<statuspanel id="statusbar-display" fixed="true"/> <!-- Make sure not to resize this element -->
|
||||
<vbox id="appcontent">
|
||||
<splitter id="abp-sidebar-splitter"/>
|
||||
<vbox id="abp-sidebar"/>
|
||||
</vbox>
|
||||
</overlay>
|
||||
@@ -0,0 +1,154 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<bindings id="progressBarBindings"
|
||||
xmlns="http://www.mozilla.org/xbl"
|
||||
xmlns:xbl="http://www.mozilla.org/xbl"
|
||||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml">
|
||||
<binding id="progressBar">
|
||||
<content orient="horizontal" pack="center">
|
||||
<xul:stack flex="1">
|
||||
<html:canvas anonid="canvas" width="1" height="1"/>
|
||||
<children/>
|
||||
</xul:stack>
|
||||
</content>
|
||||
<implementation>
|
||||
<field name="_gapWidth">5</field>
|
||||
<field name="_arrowheadWidth">5</field>
|
||||
<field name="_canvas">document.getAnonymousElementByAttribute(this, "anonid", "canvas")</field>
|
||||
|
||||
<constructor>
|
||||
<![CDATA[
|
||||
// Run _init() after window loads, correct sizes might be unknown during construction
|
||||
let me = this;
|
||||
let callback = function()
|
||||
{
|
||||
window.removeEventListener("load", callback, false);
|
||||
window.setTimeout(function() me._init(), 0);
|
||||
}
|
||||
window.addEventListener("load", callback, false);
|
||||
]]>
|
||||
</constructor>
|
||||
|
||||
<method name="_init">
|
||||
<body>
|
||||
<![CDATA[
|
||||
let canvas = this._canvas;
|
||||
let width = canvas.width = canvas.offsetWidth;
|
||||
let height = canvas.height = canvas.offsetHeight;
|
||||
|
||||
let context = canvas.getContext("2d");
|
||||
context.fillStyle = window.getComputedStyle(this, "").color;
|
||||
context.strokeStyle = window.getComputedStyle(this, "").color;
|
||||
context.lineWidth = 1;
|
||||
|
||||
let panelCount = this.childNodes.length;
|
||||
let panelWidth = (width - this._gapWidth * (panelCount - 1) - 1) / panelCount;
|
||||
for (let i = 0; i < panelCount; i++)
|
||||
{
|
||||
context.save();
|
||||
context.translate(Math.round(i * (panelWidth + this._gapWidth)) + 0.5, 0.5);
|
||||
context.beginPath();
|
||||
if (i)
|
||||
context.moveTo(-this._arrowheadWidth, 0);
|
||||
else
|
||||
context.moveTo(0, 0);
|
||||
context.lineTo(panelWidth - this._arrowheadWidth, 0);
|
||||
context.lineTo(panelWidth, (height - 1) / 2);
|
||||
context.lineTo(panelWidth - this._arrowheadWidth, height - 1);
|
||||
if (i)
|
||||
{
|
||||
context.lineTo(-this._arrowheadWidth, height - 1);
|
||||
context.lineTo(0, (height - 1) / 2);
|
||||
context.lineTo(-this._arrowheadWidth, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.lineTo(0, height - 1);
|
||||
context.lineTo(0, 0);
|
||||
}
|
||||
context.stroke();
|
||||
context.restore();
|
||||
|
||||
let childLeft = Math.round(i * (panelWidth + this._gapWidth) + 1);
|
||||
let childWidth = panelWidth - this._arrowheadWidth - 2;
|
||||
let child = this.childNodes[i];
|
||||
child.style.marginLeft = childLeft + "px";
|
||||
child.style.marginRight = (width - childLeft - childWidth) + "px";
|
||||
child.style.width = childWidth + "px";
|
||||
}
|
||||
|
||||
// Resize after initialization should be ignored
|
||||
canvas.parentNode.removeAttribute("flex");
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<property name="activeItem">
|
||||
<getter>
|
||||
<![CDATA[
|
||||
for (let i = 0; i < this.childNodes.length; i++)
|
||||
{
|
||||
let child = this.childNodes[i];
|
||||
if (/\bactive\b/.test(child.className))
|
||||
return child;
|
||||
}
|
||||
return null;
|
||||
]]>
|
||||
</getter>
|
||||
|
||||
<setter>
|
||||
<![CDATA[
|
||||
let complete = true;
|
||||
for (let i = 0; i < this.childNodes.length; i++)
|
||||
{
|
||||
let child = this.childNodes[i];
|
||||
if (child == val)
|
||||
complete = false;
|
||||
|
||||
if (!complete && child.value[0] == "✔")
|
||||
child.value = child.value.replace(/^✔\s*/, "");
|
||||
else if (complete && child.value[0] != "✔")
|
||||
child.value = "✔ " + child.value;
|
||||
|
||||
if (child != val && /\bactive\b/.test(child.className))
|
||||
child.className = child.className.replace(/\s*\bactive\b/, "");
|
||||
else if (child == val && !/\bactive\b/.test(child.className))
|
||||
child.className += " active";
|
||||
}
|
||||
return null;
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
|
||||
<property name="activeItemComplete">
|
||||
<getter>
|
||||
<![CDATA[
|
||||
let activeItem = this.activeItem;
|
||||
if (!activeItem)
|
||||
return false;
|
||||
else
|
||||
return activeItem.value[0] == "✔";
|
||||
]]>
|
||||
</getter>
|
||||
|
||||
<setter>
|
||||
<![CDATA[
|
||||
let activeItem = this.activeItem;
|
||||
if (!activeItem)
|
||||
return;
|
||||
|
||||
if (!val && activeItem.value[0] == "✔")
|
||||
activeItem.value = child.value.replace(/^✔\s*/, "");
|
||||
else if (val && activeItem.value[0] != "✔")
|
||||
activeItem.value = "✔ " + activeItem.value;
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
</implementation>
|
||||
</binding>
|
||||
</bindings>
|
||||
@@ -0,0 +1,248 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://global/skin/tree.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://@ADDON_CHROME_NAME@/skin/sendReport.css" type="text/css"?>
|
||||
|
||||
<!DOCTYPE dialog [
|
||||
<!ENTITY % reporterDTD SYSTEM "chrome://@ADDON_CHROME_NAME@/locale/sendReport.dtd">
|
||||
%reporterDTD;
|
||||
<!ENTITY % filtersDTD SYSTEM "chrome://@ADDON_CHROME_NAME@/locale/filters.dtd">
|
||||
%filtersDTD;
|
||||
]>
|
||||
|
||||
<wizard
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
title="&wizard.title;"
|
||||
id="abpSendReportWizard"
|
||||
onload="initWizard();"
|
||||
width="800"
|
||||
height="550"
|
||||
sendbuttonlabel="&sendButton.label;"
|
||||
sendbuttonaccesskey="&sendButton.accesskey;"
|
||||
windowtype="abp:sendReport">
|
||||
|
||||
<script type="application/x-javascript;version=1.7" src="utils.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="sendReport.js"/>
|
||||
|
||||
<keyset id="wizardKeys">
|
||||
<key id="undoKey" modifiers="accel" key="Z" oncommand="if (document.documentElement.currentPage.id == 'screenshotPage') screenshotDataSource.undo();"/>
|
||||
</keyset>
|
||||
|
||||
<box hidden="true">
|
||||
<xbl:bindings id="headerBindings" xmlns:xbl="http://www.mozilla.org/xbl">
|
||||
<xbl:binding id="headerBinding">
|
||||
<xbl:content orient="vertical">
|
||||
<deck xbl:inherits="selectedIndex=viewIndex">
|
||||
<description class="wizard-header-label" xbl:inherits="value=label"/>
|
||||
<progressbar id="progressBar" style="-moz-binding: url(progressBar.xml#progressBar);">
|
||||
<label id="typeSelectorHeader" class="progressLabel" value="&typeSelector.heading;" crop="end"/>
|
||||
<label id="screenshotHeader" class="progressLabel" value="&screenshot.heading;" crop="end"/>
|
||||
<label id="commentPageHeader" class="progressLabel" value="&commentPage.heading;" crop="end"/>
|
||||
<label id="sendPageHeader" class="progressLabel" value="&sendPage.heading;" crop="end"/>
|
||||
</progressbar>
|
||||
</deck>
|
||||
</xbl:content>
|
||||
</xbl:binding>
|
||||
</xbl:bindings>
|
||||
|
||||
<label id="privacyLink" class="text-link" value="&privacyPolicy.label;" onclick="Utils.loadDocLink('reporter_privacy');"/>
|
||||
</box>
|
||||
|
||||
<wizardpage id="dataCollectorPage" pageid="dataCollector" next="typeSelector" label="&dataCollector.heading;" onpageshow="initDataCollectorPage();">
|
||||
<description>&dataCollector.description;</description>
|
||||
|
||||
<progressmeter id="dataCollectorProgress" mode="undetermined"/>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="typeSelectorPage" pageid="typeSelector" next="screenshot" label="&typeSelector.heading;" onpageshow="initTypeSelectorPage();">
|
||||
<description>&typeSelector.description;</description>
|
||||
|
||||
<radiogroup id="typeGroup" oncommand="typeSelectionUpdated();">
|
||||
<radio id="typeFalsePositive" value="false positive" label="&typeSelector.falsePositive.label;" accesskey="&typeSelector.falsePositive.accesskey;"/>
|
||||
<description class="radioDescription">&typeSelector.falsePositive.description;</description>
|
||||
<radio id="typeFalseNegative" value="false negative" label="&typeSelector.falseNegative.label;" accesskey="&typeSelector.falseNegative.accesskey;"/>
|
||||
<description class="radioDescription">&typeSelector.falseNegative.description;</description>
|
||||
<radio id="typeOther" value="other" label="&typeSelector.other.label;" accesskey="&typeSelector.other.accesskey;"/>
|
||||
<description class="radioDescription">&typeSelector.other.description;</description>
|
||||
</radiogroup>
|
||||
|
||||
<deck id="recentReports" currentIndex="0" flex="1">
|
||||
<vbox pack="end">
|
||||
<label class="text-link" value="&showRecentReports.label;" onclick="E('recentReports').selectedIndex = 1;"/>
|
||||
</vbox>
|
||||
<groupbox flex="1">
|
||||
<caption label="&recentReports.label;"/>
|
||||
<grid flex="1" id="recentReportsList">
|
||||
<columns>
|
||||
<column flex="2"/>
|
||||
<column flex="1"/>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows id="recentReportsRows" onclick="reportsListDataSource.handleClick(event);"/>
|
||||
</grid>
|
||||
|
||||
<hbox pack="start">
|
||||
<button label="&recentReports.clear.label;" accesskey="&recentReports.clear.accesskey;" oncommand="reportsListDataSource.clear();"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
</deck>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="updatePage" pageid="update" next="screenshot" onpageshow="subscriptionUpdateDataSource.showPage();" reloadButtonLabel="&reloadButton.label;" reloadButtonAccesskey="&reloadButton.accesskey;">
|
||||
<vbox id="updateInProgress">
|
||||
<description>&update.inProgress.description;</description>
|
||||
<progressmeter mode="undetermined"/>
|
||||
</vbox>
|
||||
|
||||
<description id="updateFixedIssue" hidden="true">&update.fixed.description;</description>
|
||||
|
||||
<vbox id="outdatedSubscriptions">
|
||||
<description>&outdatedSubscriptions.description;</description>
|
||||
|
||||
<description id="outdatedSubscriptionTemplate" class="text-link" onclick="Utils.loadInBrowser(this.getAttribute('_url'));"/>
|
||||
|
||||
<vbox id="outdatedSubscriptionsList"/>
|
||||
|
||||
<hbox>
|
||||
<button label="&update.start.label;" oncommand="subscriptionUpdateDataSource.updateOutdated();window.close();"/>
|
||||
<button label="&issues.openPreferences.label;" oncommand="Utils.openFiltersDialog();window.close();"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="issuesPage" pageid="issues" next="screenshot" onpageshow="initIssuesPage();" reloadButtonLabel="&reloadButton.label;" reloadButtonAccesskey="&reloadButton.accesskey;">
|
||||
<description>&issues.description;</description>
|
||||
|
||||
<vbox id="issuesBox" flex="1">
|
||||
<groupbox id="issuesWhitelistBox" hidden="true">
|
||||
<description>&issues.whitelist.description;</description>
|
||||
<hbox pack="end">
|
||||
<button label="&issues.whitelist.remove.label;" oncommand="issuesDataSource.removeWhitelist();"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
<groupbox id="issuesDisabledBox" hidden="true">
|
||||
<description>&issues.disabled.description;</description>
|
||||
<hbox pack="end">
|
||||
<button label="&issues.disabled.enable.label;" oncommand="issuesDataSource.enable();"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
<groupbox id="issuesNoFiltersBox" hidden="true">
|
||||
<description>&issues.nofilters.description;</description>
|
||||
</groupbox>
|
||||
<groupbox id="issuesNoSubscriptionsBox" hidden="true">
|
||||
<description>&issues.nosubscriptions.description;</description>
|
||||
<hbox pack="end">
|
||||
<button label="&issues.nosubscriptions.add.label;" oncommand="issuesDataSource.addSubscription();"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
<groupbox id="issuesSubscriptionCountBox" hidden="true">
|
||||
<description>&issues.subscriptionCount.description;</description>
|
||||
<hbox pack="end">
|
||||
<button label="&issues.openPreferences.label;" oncommand="Utils.openFiltersDialog();window.close();"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
<groupbox id="issuesOwnFiltersBox" hidden="true">
|
||||
<description>&issues.ownfilters.description;</description>
|
||||
<hbox id="issuesOwnFiltersTemplate" align="center" hidden="true">
|
||||
<description flex="1" crop="end"/>
|
||||
<button label="&issues.ownfilters.disable.label;" oncommand="issuesDataSource.disableFilter(this.parentNode);"/>
|
||||
</hbox>
|
||||
<vbox id="issuesOwnFilters"/>
|
||||
</groupbox>
|
||||
<groupbox id="issuesDisabledSubscriptionsBox" hidden="true">
|
||||
<description>&issues.disabledgroups.description;</description>
|
||||
<hbox id="issuesDisabledSubscriptionsTemplate" align="center" hidden="true">
|
||||
<description flex="1" crop="end"/>
|
||||
<button label="&issues.disabledgroups.enable.label;" oncommand="issuesDataSource.enableSubscription(this.parentNode);"/>
|
||||
</hbox>
|
||||
<vbox id="issuesDisabledSubscriptions"/>
|
||||
</groupbox>
|
||||
<groupbox id="issuesDisabledFiltersBox" hidden="true">
|
||||
<description>&issues.disabledfilters.description;</description>
|
||||
<hbox id="issuesDisabledFiltersTemplate" align="center" hidden="true">
|
||||
<description flex="1" crop="end"/>
|
||||
<button label="&issues.disabledfilters.enable.label;" oncommand="issuesDataSource.enableFilter(this.parentNode);"/>
|
||||
</hbox>
|
||||
<vbox id="issuesDisabledFilters"/>
|
||||
</groupbox>
|
||||
</vbox>
|
||||
|
||||
<checkbox id="issuesOverride" label="&issues.override.label;" accesskey="&issues.override.accesskey;" oncommand="updateIssuesOverride();"/>
|
||||
<description id="issuesChangeMessage" hidden="true">&issues.change.description;</description>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="typeWarningPage" pageid="typeWarning" next="screenshot" onpageshow="initTypeWarningPage();">
|
||||
<description id="typeWarningText">
|
||||
&typeWarning.description;
|
||||
<label id="typeWarningTextLink" class="text-link" onclick="Utils.loadDocLink('reporter_other_link');"/>
|
||||
</description>
|
||||
|
||||
<checkbox id="typeWarningOverride" label="&typeWarning.override.label;" accesskey="&typeWarning.override.accesskey;" oncommand="updateTypeWarningOverride();"/>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="screenshotPage" pageid="screenshot" next="comment" label="&screenshot.heading;" onpageshow="initScreenshotPage();">
|
||||
<description>&screenshot.description;</description>
|
||||
|
||||
<checkbox id="screenshotCheckbox" checked="true" label="&screenshot.attach.label;" accesskey="&screenshot.attach.accesskey;" oncommand="screenshotDataSource.enabled = this.checked;"/>
|
||||
<hbox id="screenshotButtons" pack="end">
|
||||
<button id="screenshotMarkButton" type="radio" group="selectionType" oncommand="screenshotDataSource.selectionType = 'mark';" checked="true" label="&screenshot.mark.label;" accesskey="&screenshot.mark.accesskey;"/>
|
||||
<button id="screenshotRemoveButton" type="radio" group="selectionType" oncommand="screenshotDataSource.selectionType = 'remove';" label="&screenshot.remove.label;" accesskey="&screenshot.remove.accesskey;"/>
|
||||
<button id="screenshotUndoButton" oncommand="screenshotDataSource.undo();" disabled="true" label="&screenshot.undo.label;" accesskey="&screenshot.undo.accesskey;"/>
|
||||
</hbox>
|
||||
<vbox id="screenshotBox" flex="1">
|
||||
<canvas xmlns="http://www.w3.org/1999/xhtml" id="screenshotCanvas" onmousedown="screenshotDataSource.startSelection(event);" onmouseup="screenshotDataSource.stopSelection(event);" onmouseout="screenshotDataSource.stopSelection(event);" onmousemove="screenshotDataSource.updateSelection(event);"/>
|
||||
</vbox>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="commentPage" pageid="comment" next="send" label="&commentPage.heading;" onpageshow="initCommentPage();">
|
||||
<description>&commentPage.description;</description>
|
||||
|
||||
<label class="topLabel" control="comment" value="&comment.label;" accesskey="&comment.accesskey;"/>
|
||||
<textbox id="comment" multiline="true" flex="1" oninput="updateComment();"/>
|
||||
<hbox align="baseline">
|
||||
<label control="email" value="&email.label;" accesskey="&email.accesskey;"/>
|
||||
<textbox id="email" flex="1" maxlength="200" oninput="updateEmail();"/>
|
||||
</hbox>
|
||||
<description id="commentLengthWarning" visible="false">&comment.lengthWarning;</description>
|
||||
|
||||
<checkbox id="extensionsCheckbox" label="&attachExtensions.label;" accesskey="&attachExtensions.accesskey;" oncommand="updateExtensions(this.checked);"/>
|
||||
|
||||
<deck id="dataDeck" selectedIndex="0" flex="2">
|
||||
<vbox pack="start">
|
||||
<label class="text-link" value="&showData.label;" onclick="showDataField();"/>
|
||||
</vbox>
|
||||
<vbox>
|
||||
<label control="data" value="&data.label;" accesskey="&data.accesskey;"/>
|
||||
<textbox id="data" readonly="true" multiline="true" wrap="off" flex="1"/>
|
||||
</vbox>
|
||||
</deck>
|
||||
</wizardpage>
|
||||
|
||||
<wizardpage id="sendPage" pageid="send" label="&sendPage.heading;" onpageshow="initSendPage();">
|
||||
<description id="sendReportMessage">&sendPage.waitMessage;</description>
|
||||
|
||||
<vbox id="sendReportErrorBox" align="end" hidden="true">
|
||||
<description id="sendReportError" textTemplate="&sendPage.errorMessage;" defaultError="&subscription.lastDownload.connectionError;">
|
||||
<label id="sendReportErrorLinks" class="text-link" onclick="Utils.loadDocLink('reporter_connect_issue');"/>
|
||||
</description>
|
||||
<button id="sendRetryButton" label="&sendPage.retry.label;" oncommand="initSendPage();"/>
|
||||
</vbox>
|
||||
|
||||
<progressmeter id="sendReportProgress" mode="undetermined"/>
|
||||
|
||||
<iframe id="result" type="content" flex="1" hidden="true" onclick="processLinkClick(event);"
|
||||
confirmationMessage="&sendPage.confirmation;" knownIssueMessage="&sendPage.knownIssue;"/>
|
||||
|
||||
<hbox id="copyLinkBox" pack="end" hidden="true">
|
||||
<button id="copyLink" disabled="true" label="©Link.label;" accesskey="©Link.accesskey;" oncommand="copyLink(this.getAttribute('url'));"/>
|
||||
</hbox>
|
||||
</wizardpage>
|
||||
|
||||
</wizard>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<!DOCTYPE vbox SYSTEM "chrome://adblockplus/locale/overlay.dtd">
|
||||
|
||||
<window
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
width="1"
|
||||
height="1"
|
||||
onload="window.close();AppIntegration.openMenu(window.opener);">
|
||||
<script type="application/x-javascript;version=1.7" src="utils.js"/>
|
||||
|
||||
<setting type="control">
|
||||
<button id="adblockplus-filters" label="&filters.label;…"/>
|
||||
</setting>
|
||||
<setting pref="extensions.@ADDON_CHROME_NAME@.enabled" type="bool" inverted="true" title="&disable.label;"/>
|
||||
<setting pref="extensions.@ADDON_CHROME_NAME@.frameobjects" type="bool" title="&objecttabs.label;"/>
|
||||
<setting pref="extensions.@ADDON_CHROME_NAME@.fastcollapse" type="bool" inverted="true" title="&hideplaceholders.label;"/>
|
||||
<setting id="adblockplus-savestats" type="bool" title="&counthits.label;"/>
|
||||
<setting id="adblockplus-sync" type="bool" title="&sync.label;"/>
|
||||
<setting id="adblockplus-showinaddonbar" type="bool" title="&showinaddonbar.label;"/>
|
||||
<setting id="adblockplus-showintoolbar" type="bool" title="&showintoolbar.label;"/>
|
||||
<setting id="adblockplus-showinstatusbar" pref="extensions.@ADDON_CHROME_NAME@.showinstatusbar" type="bool" title="&showinstatusbar.label;"/>
|
||||
</window>
|
||||
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://@ADDON_CHROME_NAME@/skin/sidebar.css" type="text/css"?>
|
||||
|
||||
<!DOCTYPE page SYSTEM "chrome://@ADDON_CHROME_NAME@/locale/sidebar.dtd">
|
||||
|
||||
<page
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
id="abp-sidebar"
|
||||
onload="init()"
|
||||
onunload="cleanUp()"
|
||||
docDomainThirdParty="&docDomain.thirdParty;"
|
||||
docDomainFirstParty="&docDomain.firstParty;">
|
||||
|
||||
<script type="application/x-javascript;version=1.7" src="utils.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="sidebar.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="flasher.js"/>
|
||||
|
||||
<keyset id="sidebarKeys">
|
||||
<key id="block-key" keycode="VK_ENTER"/>
|
||||
<key id="copy-key" modifiers="accel" key="C" command="copy-command"/>
|
||||
<key id="selectAll-key" modifiers="accel" key="A" command="selectAll-command"/>
|
||||
</keyset>
|
||||
|
||||
<commandset id="sidebarCommands">
|
||||
<command id="copy-command" oncommand="copyToClipboard()" disabled="true"/>
|
||||
<command id="selectAll-command" oncommand="selectAll()"/>
|
||||
</commandset>
|
||||
|
||||
<popupset id="sidebarPopups">
|
||||
<tooltip id="tooltip" orient="vertical" onpopupshowing="fillInTooltip(event);">
|
||||
<description id="tooltipDummy"/>
|
||||
<hbox id="tooltipPreviewBox" pack="start">
|
||||
<image id="tooltipPreview" validate="never"/>
|
||||
</hbox>
|
||||
<grid>
|
||||
<columns>
|
||||
<column/>
|
||||
<column flex="1"/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row id="tooltipAddressRow" align="top">
|
||||
<label value="&tooltip.address.label;"/>
|
||||
<vbox id="tooltipAddress"/>
|
||||
</row>
|
||||
<row id="tooltipTypeRow">
|
||||
<label value="&tooltip.type.label;"/>
|
||||
<description id="tooltipType" filtered="&tooltip.type.blocked;" whitelisted="&tooltip.type.whitelisted;"/>
|
||||
</row>
|
||||
<row id="tooltipSizeRow">
|
||||
<label value="&tooltip.size.label;"/>
|
||||
<description id="tooltipSize"/>
|
||||
</row>
|
||||
<row id="tooltipDocDomainRow">
|
||||
<label value="&tooltip.docDomain.label;"/>
|
||||
<description id="tooltipDocDomain"/>
|
||||
</row>
|
||||
<row id="tooltipFilterRow" align="top">
|
||||
<label value="&tooltip.filter.label;"/>
|
||||
<vbox id="tooltipFilter" disabledText="&tooltip.filter.disabled;"/>
|
||||
</row>
|
||||
<row id="tooltipFilterSourceRow" align="top">
|
||||
<label value="&tooltip.filterSource.label;"/>
|
||||
<vbox id="tooltipFilterSource"/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
</tooltip>
|
||||
|
||||
<menupopup id="context" onpopupshowing="return fillInContext(event)">
|
||||
<menuitem id="contextBlock" label="&context.block.label;…" oncommand="doBlock()" key="block-key"/>
|
||||
<menuitem id="contextWhitelist" label="&context.whitelist.label;…" oncommand="doBlock()" key="block-key"/>
|
||||
<menuitem id="contextEditFilter" label="&context.editfilter.label;…" oncommand="editFilter()"/>
|
||||
<menuitem id="contextDisableFilter" labeltempl="&context.disablefilter.label;" oncommand="enableFilter(treeView.getSelectedItem().filter, false)"/>
|
||||
<menuitem id="contextEnableFilter" labeltempl="&context.enablefilter.label;" oncommand="enableFilter(treeView.getSelectedItem().filter, true)"/>
|
||||
<menuitem id="contextDisableOnSite" labeltempl="&context.disablefilteronsite.label;" oncommand="disableOnSite()"/>
|
||||
<menuseparator id="contextOpenSep"/>
|
||||
<menuitem id="contextOpen" label="&context.open.label;" oncommand="openInTab(null, event)"/>
|
||||
<menuitem id="contextFlash" label="&context.flash.label;" oncommand="onSelectionChange()"/>
|
||||
<menuitem id="contextCopy" label="&context.copy.label;" command="copy-command" key="copy-key"/>
|
||||
<menuitem id="contextCopyFilter" label="&context.copyFilter.label;" oncommand="copyFilter()"/>
|
||||
<menuseparator id="contextSelectSep"/>
|
||||
<menuitem id="contextSelectAll" label="&context.selectAll.label;" command="selectAll-command" key="selectAll-key"/>
|
||||
</menupopup>
|
||||
</popupset>
|
||||
|
||||
<hbox>
|
||||
<hbox align="center" flex="1">
|
||||
<label value="&search.label;" accesskey="&search.accesskey;" control="searchField"/>
|
||||
<textbox id="searchField" flex="1" type="search" oncommand="treeView.setFilter(this.value)"/>
|
||||
</hbox>
|
||||
<description id="detachButton" value="&detach.label;" onclick="detach(true)"/>
|
||||
<description id="reattachButton" value="&reattach.label;" onclick="if (this.getAttribute('disabled') != 'true') detach(false)" hidden="true"/>
|
||||
</hbox>
|
||||
|
||||
<tree id="list" context="context" flex="1" seltype="multiple" enableColumnDrag="true"
|
||||
defaultSort="state descending" persist="defaultSort"
|
||||
onkeypress="if (event.keyCode == event.DOM_VK_RETURN || event.keyCode == event.DOM_VK_ENTER) doBlock()">
|
||||
<treecols>
|
||||
<treecol id="address" label="&address.label;" flex="2" crop="center" persist="width ordinal sortDirection hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="filter" label="&filter.label;" flex="1" persist="width ordinal sortDirection hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="type" label="&type.label;" width="80" persist="width ordinal sortDirection hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="state" label="&state.label;" width="16" persist="width ordinal sortDirection hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="size" label="&size.label;" width="60" hidden="true" persist="width ordinal sortDirection hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="docDomain" label="&docDomain.label;" width="100" hidden="true" persist="width ordinal sortDirection hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="filterSource" label="&filterSource.label;" width="100" hidden="true" persist="width ordinal sortDirection hidden"/>
|
||||
</treecols>
|
||||
<treechildren id="treechildren"
|
||||
tooltip="tooltip"
|
||||
onclick="handleClick(event)"
|
||||
ondblclick="handleDblClick(event)"
|
||||
noitemslabel="&noitems.label;"
|
||||
whitelistedlabel="&whitelisted.label;"/>
|
||||
</tree>
|
||||
</page>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
|
||||
<!DOCTYPE page SYSTEM "chrome://@ADDON_CHROME_NAME@/locale/sidebar.dtd">
|
||||
|
||||
<window
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
id="abpDetachedSidebar"
|
||||
title="&detached.title;"
|
||||
persist="screenX screenY width height sizemode"
|
||||
onclose="document.getElementById('abp-command-sidebar').doCommand(); return false;">
|
||||
|
||||
<script type="application/x-javascript">
|
||||
// Some people actually switch off browser.frames.enabled and are surprised
|
||||
// that things stop working...
|
||||
window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIWebNavigation)
|
||||
.QueryInterface(Components.interfaces.nsIDocShell)
|
||||
.allowSubframes = true;
|
||||
</script>
|
||||
|
||||
<keyset id="detached-keyset">
|
||||
<key keycode="VK_ESCAPE" command="abp-command-sidebar"/>
|
||||
<key modifiers="accel" key="w" command="abp-command-sidebar"/>
|
||||
</keyset>
|
||||
|
||||
<commandset id="detached-commandset">
|
||||
<command id="abp-command-sidebar" oncommand="document.getElementById('sidebarFrame').contentWindow.doClose()"/>
|
||||
</commandset>
|
||||
|
||||
<iframe src="sidebar.xul" id="sidebarFrame" flex="1"/>
|
||||
</window>
|
||||
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
let subscriptionListLoading = false;
|
||||
|
||||
function init()
|
||||
{
|
||||
if (window.arguments && window.arguments.length && window.arguments[0])
|
||||
{
|
||||
let source = window.arguments[0];
|
||||
setCustomSubscription(source.title, source.url,
|
||||
source.mainSubscriptionTitle, source.mainSubscriptionURL);
|
||||
|
||||
E("all-subscriptions-container").hidden = true;
|
||||
E("fromWebText").hidden = false;
|
||||
}
|
||||
else
|
||||
loadSubscriptionList();
|
||||
}
|
||||
|
||||
function updateSubscriptionInfo()
|
||||
{
|
||||
let selectedSubscription = E("all-subscriptions").selectedItem;
|
||||
|
||||
E("subscriptionInfo").setAttribute("invisible", !selectedSubscription);
|
||||
if (selectedSubscription)
|
||||
{
|
||||
let url = selectedSubscription.getAttribute("_url");
|
||||
let homePage = selectedSubscription.getAttribute("_homepage")
|
||||
|
||||
let viewLink = E("view-list");
|
||||
viewLink.setAttribute("_url", url);
|
||||
viewLink.setAttribute("tooltiptext", url);
|
||||
|
||||
let homePageLink = E("visit-homepage");
|
||||
homePageLink.hidden = !homePage;
|
||||
if (homePage)
|
||||
{
|
||||
homePageLink.setAttribute("_url", homePage);
|
||||
homePageLink.setAttribute("tooltiptext", homePage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reloadSubscriptionList()
|
||||
{
|
||||
subscriptionListLoading = false;
|
||||
loadSubscriptionList();
|
||||
}
|
||||
|
||||
function loadSubscriptionList()
|
||||
{
|
||||
if (subscriptionListLoading)
|
||||
return;
|
||||
|
||||
E("all-subscriptions-container").selectedIndex = 0;
|
||||
E("all-subscriptions-loading").hidden = false;
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
let errorHandler = function()
|
||||
{
|
||||
E("all-subscriptions-container").selectedIndex = 2;
|
||||
E("all-subscriptions-loading").hidden = true;
|
||||
};
|
||||
let successHandler = function()
|
||||
{
|
||||
if (!request.responseXML || request.responseXML.documentElement.localName != "subscriptions")
|
||||
{
|
||||
errorHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
processSubscriptionList(request.responseXML);
|
||||
E("all-subscriptions").selectedIndex = 0;
|
||||
E("all-subscriptions").focus();
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
errorHandler();
|
||||
}
|
||||
};
|
||||
|
||||
request.open("GET", Prefs.subscriptions_listurl);
|
||||
request.addEventListener("error", errorHandler, false);
|
||||
request.addEventListener("load", successHandler, false);
|
||||
request.send(null);
|
||||
|
||||
subscriptionListLoading = true;
|
||||
}
|
||||
|
||||
function processSubscriptionList(doc)
|
||||
{
|
||||
let list = E("all-subscriptions");
|
||||
while (list.firstChild)
|
||||
list.removeChild(list.firstChild);
|
||||
|
||||
addSubscriptions(list, doc.documentElement, 0, null, null);
|
||||
E("all-subscriptions-container").selectedIndex = 1;
|
||||
E("all-subscriptions-loading").hidden = true;
|
||||
}
|
||||
|
||||
function addSubscriptions(list, parent, level, parentTitle, parentURL)
|
||||
{
|
||||
for (let i = 0; i < parent.childNodes.length; i++)
|
||||
{
|
||||
let node = parent.childNodes[i];
|
||||
if (node.nodeType != Node.ELEMENT_NODE || node.localName != "subscription")
|
||||
continue;
|
||||
|
||||
if (node.getAttribute("type") != "ads" || node.getAttribute("deprecated") == "true")
|
||||
continue;
|
||||
|
||||
let variants = node.getElementsByTagName("variants");
|
||||
if (!variants.length || !variants[0].childNodes.length)
|
||||
continue;
|
||||
variants = variants[0].childNodes;
|
||||
|
||||
let isFirst = true;
|
||||
let mainTitle = null;
|
||||
let mainURL = null;
|
||||
for (let j = 0; j < variants.length; j++)
|
||||
{
|
||||
let variant = variants[j];
|
||||
if (variant.nodeType != Node.ELEMENT_NODE || variant.localName != "variant")
|
||||
continue;
|
||||
|
||||
let item = document.createElement("richlistitem");
|
||||
item.setAttribute("_title", variant.getAttribute("title"));
|
||||
item.setAttribute("_url", variant.getAttribute("url"));
|
||||
if (parentTitle && parentURL && variant.getAttribute("complete") != "true")
|
||||
{
|
||||
item.setAttribute("_supplementForTitle", parentTitle);
|
||||
item.setAttribute("_supplementForURL", parentURL);
|
||||
}
|
||||
item.setAttribute("tooltiptext", variant.getAttribute("url"));
|
||||
item.setAttribute("_homepage", node.getAttribute("homepage"));
|
||||
|
||||
let title = document.createElement("description");
|
||||
if (isFirst)
|
||||
{
|
||||
if (Utils.checkLocalePrefixMatch(node.getAttribute("prefixes")))
|
||||
title.setAttribute("class", "subscriptionTitle localeMatch");
|
||||
else
|
||||
title.setAttribute("class", "subscriptionTitle");
|
||||
title.textContent = node.getAttribute("title") + " (" + node.getAttribute("specialization") + ")";
|
||||
mainTitle = variant.getAttribute("title");
|
||||
mainURL = variant.getAttribute("url");
|
||||
isFirst = false;
|
||||
}
|
||||
title.setAttribute("flex", "1");
|
||||
title.style.marginLeft = (20 * level) + "px";
|
||||
item.appendChild(title);
|
||||
|
||||
let variantTitle = document.createElement("description");
|
||||
variantTitle.setAttribute("class", "variant");
|
||||
variantTitle.textContent = variant.getAttribute("title");
|
||||
variantTitle.setAttribute("crop", "end");
|
||||
item.appendChild(variantTitle);
|
||||
|
||||
list.appendChild(item);
|
||||
}
|
||||
|
||||
let supplements = node.getElementsByTagName("supplements");
|
||||
if (supplements.length)
|
||||
addSubscriptions(list, supplements[0], level + 1, mainTitle, mainURL);
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectionChange()
|
||||
{
|
||||
let selectedItem = E("all-subscriptions").selectedItem;
|
||||
if (!selectedItem)
|
||||
return;
|
||||
|
||||
setCustomSubscription(selectedItem.getAttribute("_title"), selectedItem.getAttribute("_url"),
|
||||
selectedItem.getAttribute("_supplementForTitle"), selectedItem.getAttribute("_supplementForURL"));
|
||||
|
||||
updateSubscriptionInfo();
|
||||
}
|
||||
|
||||
function setCustomSubscription(title, url, mainSubscriptionTitle, mainSubscriptionURL)
|
||||
{
|
||||
E("title").value = title;
|
||||
E("location").value = url;
|
||||
|
||||
let messageElement = E("supplementMessage");
|
||||
let addMainCheckbox = E("addMainSubscription");
|
||||
if (mainSubscriptionURL && !hasSubscription(mainSubscriptionURL))
|
||||
{
|
||||
messageElement.removeAttribute("invisible");
|
||||
addMainCheckbox.removeAttribute("invisible");
|
||||
|
||||
let [, beforeLink, afterLink] = /(.*)\?1\?(.*)/.exec(messageElement.getAttribute("_textTemplate")) || [null, messageElement.getAttribute("_textTemplate"), ""];
|
||||
while (messageElement.firstChild)
|
||||
messageElement.removeChild(messageElement.firstChild);
|
||||
messageElement.appendChild(document.createTextNode(beforeLink));
|
||||
let link = document.createElement("label");
|
||||
link.className = "text-link";
|
||||
link.setAttribute("tooltiptext", mainSubscriptionURL);
|
||||
link.addEventListener("click", function() Utils.loadInBrowser(mainSubscriptionURL), false);
|
||||
link.textContent = mainSubscriptionTitle;
|
||||
messageElement.appendChild(link);
|
||||
messageElement.appendChild(document.createTextNode(afterLink));
|
||||
|
||||
addMainCheckbox.value = mainSubscriptionURL;
|
||||
addMainCheckbox.setAttribute("_mainSubscriptionTitle", mainSubscriptionTitle)
|
||||
addMainCheckbox.label = addMainCheckbox.getAttribute("_labelTemplate").replace(/\?1\?/g, mainSubscriptionTitle);
|
||||
addMainCheckbox.accessKey = addMainCheckbox.accessKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
messageElement.setAttribute("invisible", "true");
|
||||
addMainCheckbox.setAttribute("invisible", "true");
|
||||
}
|
||||
}
|
||||
|
||||
function validateURL(url)
|
||||
{
|
||||
if (!url)
|
||||
return null;
|
||||
url = url.replace(/^\s+/, "").replace(/\s+$/, "");
|
||||
|
||||
// Is this a file path?
|
||||
try {
|
||||
let file = new FileUtils.File(url);
|
||||
return Services.io.newFileURI(file).spec;
|
||||
} catch (e) {}
|
||||
|
||||
// Is this a valid URL?
|
||||
let uri = Utils.makeURI(url);
|
||||
if (uri)
|
||||
return uri.spec;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function addSubscription()
|
||||
{
|
||||
let url = E("location").value;
|
||||
url = validateURL(url);
|
||||
if (!url)
|
||||
{
|
||||
Utils.alert(window, Utils.getString("subscription_invalid_location"));
|
||||
E("location").focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
let title = E("title").value.replace(/^\s+/, "").replace(/\s+$/, "");
|
||||
if (!title)
|
||||
title = url;
|
||||
|
||||
doAddSubscription(url, title);
|
||||
|
||||
let addMainCheckbox = E("addMainSubscription")
|
||||
if (addMainCheckbox.getAttribute("invisible") != "true" && addMainCheckbox.checked)
|
||||
{
|
||||
let mainSubscriptionTitle = addMainCheckbox.getAttribute("_mainSubscriptionTitle");
|
||||
let mainSubscriptionURL = validateURL(addMainCheckbox.value);
|
||||
if (mainSubscriptionURL)
|
||||
doAddSubscription(mainSubscriptionURL, mainSubscriptionTitle);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new subscription to the list.
|
||||
*/
|
||||
function doAddSubscription(/**String*/ url, /**String*/ title)
|
||||
{
|
||||
let subscription = Subscription.fromURL(url);
|
||||
if (!subscription)
|
||||
return;
|
||||
|
||||
FilterStorage.addSubscription(subscription);
|
||||
|
||||
subscription.disabled = false;
|
||||
subscription.title = title;
|
||||
|
||||
if (subscription instanceof DownloadableSubscription && !subscription.lastDownload)
|
||||
Synchronizer.execute(subscription);
|
||||
}
|
||||
|
||||
function hasSubscription(url)
|
||||
{
|
||||
return FilterStorage.subscriptions.some(function(subscription) subscription instanceof DownloadableSubscription && subscription.url == url);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- This Source Code is subject to the terms of the Mozilla Public License
|
||||
- version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
- http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#filter substitution
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://@ADDON_CHROME_NAME@/skin/subscriptionSelection.css" type="text/css"?>
|
||||
|
||||
<!DOCTYPE dialog SYSTEM "chrome://@ADDON_CHROME_NAME@/locale/subscriptionSelection.dtd">
|
||||
|
||||
<dialog
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
buttons="accept,cancel"
|
||||
buttonlabelaccept="&addSubscription.label;"
|
||||
title="&dialog.title;"
|
||||
id="abpSubscriptionSelection"
|
||||
windowtype="abp:subscriptionSelection"
|
||||
onload="init();"
|
||||
ondialogaccept="return addSubscription();">
|
||||
|
||||
<script type="application/x-javascript;version=1.7" src="utils.js"/>
|
||||
<script type="application/x-javascript;version=1.7" src="subscriptionSelection.js"/>
|
||||
|
||||
<deck id="all-subscriptions-container" selectedIndex="0" flex="1">
|
||||
<vbox pack="center">
|
||||
<progressmeter id="all-subscriptions-loading" mode="undetermined"/>
|
||||
</vbox>
|
||||
<vbox>
|
||||
<richlistbox id="all-subscriptions" onselect="onSelectionChange()" flex="1"/>
|
||||
<hbox id="subscriptionInfo" invisible="true">
|
||||
<label id="view-list" class="text-link" value="&viewList.label;" onclick="Utils.loadInBrowser(this.getAttribute('_url'))"/>
|
||||
<spacer flex="1"/>
|
||||
<label id="visit-homepage" class="text-link" value="&visitHomepage.label;" onclick="Utils.loadInBrowser(this.getAttribute('_url'))"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<vbox pack="center" align="center">
|
||||
<description value="&list.download.failed;"/>
|
||||
<hbox>
|
||||
<button label="&list.download.retry;" oncommand="reloadSubscriptionList()"/>
|
||||
<button label="&list.download.website;" oncommand="Utils.loadDocLink('subscriptions')"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</deck>
|
||||
|
||||
<description id="fromWebText" hidden="true">&fromWeb.description;</description>
|
||||
|
||||
<groupbox id="differentSubscription">
|
||||
<label value="&title.label;" accesskey="&title.accesskey;" control="title"/>
|
||||
<textbox id="title"/>
|
||||
|
||||
<label value="&location.label;" accesskey="&location.accesskey;" control="location"/>
|
||||
<textbox id="location"/>
|
||||
</groupbox>
|
||||
|
||||
<description id="supplementMessage" invisible="true" _textTemplate="&supplementMessage;">
|
||||
&supplementMessage;
|
||||
<label class="text-link" oncommand="">dummy dummy dummy dummy dummy dummy dummy dummy dummy dummy</label>
|
||||
</description>
|
||||
<checkbox id="addMainSubscription" invisible="true" checked="true" _labelTemplate="&addMain.label;" label="&addMain.label;" accesskey="&addMain.accesskey;"/>
|
||||
|
||||
</dialog>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<subscriptions>
|
||||
<subscription title="EasyList"
|
||||
specialization="English"
|
||||
url="https://easylist.to/easylist/easylist.txt"
|
||||
homepage="https://easylist.to/"
|
||||
prefixes="en"
|
||||
author="EasyList"/>
|
||||
<subscription title="EasyPrivacy"
|
||||
specialization="English"
|
||||
url="https://easylist.to/easylist/easyprivacy.txt"
|
||||
homepage="https://easylist.to/"
|
||||
prefixes="en"
|
||||
author="EasyList"/>
|
||||
<subscription title="Fanboy's Annoyance List"
|
||||
specialization="English"
|
||||
url="https://easylist.to/easylist/fanboy-annoyance.txt"
|
||||
homepage="https://easylist.to/"
|
||||
prefixes="en"
|
||||
author="Fanboy"/>
|
||||
<subscription title="Fanboy's Social Blocking List"
|
||||
specialization="English"
|
||||
url="https://easylist.to/easylist/fanboy-social.txt"
|
||||
homepage="https://easylist.to/"
|
||||
prefixes="en"
|
||||
author="Fanboy"/>
|
||||
</subscriptions>
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "AppIntegration.jsm");
|
||||
Cu.import(baseURL + "ContentPolicy.jsm");
|
||||
Cu.import(baseURL + "FilterClasses.jsm");
|
||||
Cu.import(baseURL + "FilterListener.jsm");
|
||||
Cu.import(baseURL + "FilterStorage.jsm");
|
||||
Cu.import(baseURL + "FilterNotifier.jsm");
|
||||
Cu.import(baseURL + "IO.jsm");
|
||||
Cu.import(baseURL + "Matcher.jsm");
|
||||
Cu.import(baseURL + "Prefs.jsm");
|
||||
Cu.import(baseURL + "RequestNotifier.jsm");
|
||||
Cu.import(baseURL + "SubscriptionClasses.jsm");
|
||||
Cu.import(baseURL + "Synchronizer.jsm");
|
||||
/* Cu.import(baseURL + "Sync.jsm"); */
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
|
||||
/**
|
||||
* Shortcut for document.getElementById(id)
|
||||
*/
|
||||
function E(id)
|
||||
{
|
||||
return document.getElementById(id);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#filter substitution
|
||||
|
||||
<!ENTITY dialog.title "About @ADDON_NAME@">
|
||||
|
||||
<!ENTITY version.title "Version">
|
||||
<!ENTITY description "
|
||||
@ADDON_NAME@ allows you to decide what you want to see on the web.
|
||||
You don't need to download adverts and banners any more; if you
|
||||
don't want them - remove them with @ADDON_NAME@!
|
||||
">
|
||||
|
||||
<!ENTITY homepage.label "@ADDON_NAME@ homepage:">
|
||||
<!ENTITY author.label "Author:">
|
||||
<!ENTITY contributors.label "Contributors:">
|
||||
<!ENTITY subscriptionAuthors.label "Filter subscription authors:">
|
||||
<!ENTITY translators.label "Translators:">
|
||||
@@ -0,0 +1,53 @@
|
||||
#filter substitution
|
||||
|
||||
<!ENTITY dialog.title "Add @ADDON_NAME@ filter rule">
|
||||
<!ENTITY accept.label "Add filter">
|
||||
<!ENTITY advanced.label "Advanced view">
|
||||
<!ENTITY basic.label "Basic view">
|
||||
|
||||
<!ENTITY disabled.warning "@ADDON_NAME@ is currently disabled. You can still add filters but they will not be applied unless you [link]enable @ADDON_NAME@[/link].">
|
||||
|
||||
<!ENTITY filter.label "New filter:">
|
||||
<!ENTITY filter.accesskey "f">
|
||||
<!ENTITY preferences.label "Show existing filters...">
|
||||
<!ENTITY preferences.accesskey "S">
|
||||
<!ENTITY type.filter.label "Blocking filter">
|
||||
<!ENTITY type.filter.accesskey "B">
|
||||
<!ENTITY type.whitelist.label "Exception rule">
|
||||
<!ENTITY type.whitelist.accesskey "x">
|
||||
<!ENTITY pattern.label "Look for pattern">
|
||||
<!ENTITY pattern.explanation "The pattern can be any part of the address; asterisks (*) act as wildcards. The filter will only be applied to addresses matching the pattern provided.">
|
||||
<!ENTITY regexp.warning "The pattern you entered will be interpreted as a regular expression which cannot be efficiently processed by @ADDON_NAME@ and may slow down your browsing experience. If you didn't intend to use a regular expression, add an asterisk (*) to the end of the pattern.">
|
||||
<!ENTITY shortpattern.warning "The pattern you entered is too short to be optimized and may slow down your browsing experience. It is recommended that you choose a longer string for this filter to allow @ADDON_NAME@ to process the filter more efficiently.">
|
||||
<!ENTITY match.warning "The pattern you entered no longer matches the address to be blocked/whitelisted and will have no effect on it.">
|
||||
<!ENTITY custom.pattern.label "Custom:">
|
||||
<!ENTITY custom.pattern.accesskey "C">
|
||||
<!ENTITY anchors.label "Accept pattern only:">
|
||||
<!ENTITY anchor.start.label "at the beginning of the address">
|
||||
<!-- Note: This access key should usually be the same as anchor.start.flexible.accesskey. It is the same checkbox with different label depending on suggested filter.-->
|
||||
<!ENTITY anchor.start.accesskey "g">
|
||||
<!ENTITY anchor.start.flexible.label "at the beginning of the domain name">
|
||||
<!-- Note: This access key should usually be the same as anchor.start.accesskey. It is the same checkbox with different label depending on suggested filter.-->
|
||||
<!ENTITY anchor.start.flexible.accesskey "g">
|
||||
<!ENTITY anchor.end.label "at the end of the address">
|
||||
<!ENTITY anchor.end.accesskey "n">
|
||||
<!ENTITY options.label "Options">
|
||||
<!ENTITY domainRestriction.label "Restrict to domain:">
|
||||
<!ENTITY domainRestriction.accesskey "d">
|
||||
<!ENTITY domainRestriction.help "Use this option to specify one or more domains separated by a bar line (|). The filter will only be applied on the domain(s) selected. A tilde (~) before a domain name indicates that the filter will not be applied on that domain.">
|
||||
<!ENTITY firstParty.label "First-party only">
|
||||
<!ENTITY firstParty.accesskey "r">
|
||||
<!ENTITY thirdParty.label "Third-party only">
|
||||
<!ENTITY thirdParty.accesskey "T">
|
||||
<!ENTITY matchCase.label "Match case">
|
||||
<!ENTITY matchCase.accesskey "M">
|
||||
<!ENTITY types.label "Apply to types:">
|
||||
<!ENTITY selectAllTypes.label "Select all">
|
||||
<!ENTITY unselectAllTypes.label "Select none">
|
||||
|
||||
<!ENTITY collapse.label "Collapse blocked:">
|
||||
<!ENTITY collapse.accesskey "l">
|
||||
<!ENTITY collapse.default.yes.label "Use default (yes)">
|
||||
<!ENTITY collapse.default.no.label "Use default (no)">
|
||||
<!ENTITY collapse.yes.label "Yes">
|
||||
<!ENTITY collapse.no.label "No">
|
||||
@@ -0,0 +1,116 @@
|
||||
#filter substitution
|
||||
|
||||
<!ENTITY dialog.title "@ADDON_NAME@ Filter Preferences">
|
||||
|
||||
<!ENTITY subscriptions.tab.label "Filter subscriptions">
|
||||
<!ENTITY filters.tab.label "Custom filters">
|
||||
|
||||
<!ENTITY addSubscription.label "Add filter subscription">
|
||||
<!ENTITY addSubscription.accesskey "f">
|
||||
|
||||
<!ENTITY addSubscriptionAdd.label "Add">
|
||||
<!ENTITY addSubscriptionCancel.label "Cancel">
|
||||
<!ENTITY addSubscriptionOther.label "Add a different subscription">
|
||||
|
||||
<!ENTITY noSubscriptions.text "
|
||||
You haven't added any filter subscriptions yet. @ADDON_NAME@ won't block
|
||||
anything without filters, please use "Add filter subscription" to
|
||||
add some.
|
||||
">
|
||||
<!ENTITY subscription.homepage.label "Homepage">
|
||||
<!ENTITY subscription.external.label "Updated by another extension">
|
||||
<!ENTITY subscription.source.label "Filter list">
|
||||
<!ENTITY subscription.enabled.label "Enabled">
|
||||
<!ENTITY subscription.lastDownload.label "Last download:">
|
||||
<!ENTITY subscription.lastDownload.inProgress "Downloading…">
|
||||
<!ENTITY subscription.lastDownload.unknown "N/A">
|
||||
<!ENTITY subscription.lastDownload.invalidURL "Failed, not a valid address">
|
||||
<!ENTITY subscription.lastDownload.connectionError "Failed, download failure">
|
||||
<!ENTITY subscription.lastDownload.invalidData "Failed, not a valid filters list">
|
||||
<!ENTITY subscription.lastDownload.checksumMismatch "Failed, checksum mismatch">
|
||||
<!ENTITY subscription.lastDownload.success "Success">
|
||||
<!ENTITY subscription.minVersion.warning "This filter subscription requires a newer @ADDON_NAME@ version, you should update to the latest @ADDON_NAME@ version.">
|
||||
<!ENTITY subscription.disabledFilters.warning "Some filters in this subscription are disabled.">
|
||||
<!ENTITY subscription.disabledFilters.enable "Enable disabled filters">
|
||||
|
||||
<!ENTITY subscription.actions.label "Actions">
|
||||
<!ENTITY subscription.update.label "Update filters">
|
||||
<!ENTITY subscription.editTitle.label "Edit title">
|
||||
<!ENTITY subscription.delete.label "Delete">
|
||||
<!ENTITY subscription.showHideFilters.label "Show/hide filters">
|
||||
<!ENTITY subscription.moveUp.label "Move up">
|
||||
<!ENTITY subscription.moveDown.label "Move down">
|
||||
|
||||
<!ENTITY acceptableAds2.label "Allow some non-intrusive advertising">
|
||||
<!ENTITY acceptableAds2.accesskey "i">
|
||||
<!ENTITY viewList.label "View list">
|
||||
<!ENTITY readMore.label "Read more">
|
||||
|
||||
<!ENTITY addGroup.label "Add filter group">
|
||||
<!ENTITY addGroup.accesskey "g">
|
||||
|
||||
<!ENTITY noFilters.text "
|
||||
You don't have any custom filters yet.
|
||||
">
|
||||
|
||||
<!ENTITY addFilter.label "Add filter">
|
||||
<!ENTITY addFilter.accesskey "d">
|
||||
|
||||
<!ENTITY filter.actions.label "Filter actions">
|
||||
<!ENTITY filter.edit.label "Edit">
|
||||
<!ENTITY filter.cut.label "Cut">
|
||||
<!ENTITY filter.copy.label "Copy">
|
||||
<!ENTITY filter.paste.label "Paste">
|
||||
<!ENTITY filter.delete.label "Delete">
|
||||
<!ENTITY filter.selectAll.label "Select All">
|
||||
<!ENTITY filter.resetHitCounts.label "Reset hit statistics">
|
||||
<!ENTITY filter.moveUp.label "Move up">
|
||||
<!ENTITY filter.moveDown.label "Move down">
|
||||
<!ENTITY viewMenu.label "View">
|
||||
|
||||
<!ENTITY filter.column "Filter rule">
|
||||
<!ENTITY filter.accesskey "F">
|
||||
<!ENTITY slow.column "Slow filters">
|
||||
<!ENTITY slow.accesskey "w">
|
||||
<!ENTITY enabled.column "Enabled">
|
||||
<!ENTITY enabled.accesskey "n">
|
||||
<!ENTITY hitcount.column "Hits">
|
||||
<!ENTITY hitcount.accesskey "H">
|
||||
<!ENTITY lasthit.column "Last hit">
|
||||
<!ENTITY lasthit.accesskey "L">
|
||||
|
||||
<!ENTITY sort.label "Sort by">
|
||||
<!ENTITY sort.accesskey "S">
|
||||
<!ENTITY sort.none.label "Unsorted">
|
||||
<!ENTITY sort.none.accesskey "U">
|
||||
<!ENTITY sort.ascending.label "A > Z sort order">
|
||||
<!ENTITY sort.ascending.accesskey "A">
|
||||
<!ENTITY sort.descending.label "Z > A sort order">
|
||||
<!ENTITY sort.descending.accesskey "Z">
|
||||
|
||||
<!ENTITY noGroupSelected.text "You need to select a filter group before its filters can be displayed.">
|
||||
<!ENTITY noFiltersInGroup.text "The selected group is empty.">
|
||||
|
||||
<!ENTITY filters.remove.warning "Do you really want to remove all selected filters?">
|
||||
|
||||
<!ENTITY backupButton.label "Backup and Restore">
|
||||
<!ENTITY backupButton.accesskey "B">
|
||||
|
||||
<!ENTITY backup.label "Create new backup">
|
||||
<!-- Note: the placeholder ?1? will be replaced by date/time of the automatic backup -->
|
||||
<!ENTITY restore.default.label "Restore backup from ?1?">
|
||||
<!ENTITY restore.own.label "Restore own backup">
|
||||
|
||||
<!ENTITY backup.complete.title "All filters and subscriptions">
|
||||
<!ENTITY backup.custom.title "Custom filters only">
|
||||
|
||||
<!ENTITY backup.error "There was an error writing filters to the file. Make sure that the file isn't write protected or in use by another application.">
|
||||
<!ENTITY restore.error "The file's data could not be processed, maybe this isn't an @ADDON_NAME@ backup file?">
|
||||
<!ENTITY restore.complete.warning "All your filter preferences will be replaced by the contents of the selected file. Do you want to proceeed?">
|
||||
<!ENTITY restore.custom.warning "All your custom filters will be replaced by the contents of the selected file. Do you want to proceeed?">
|
||||
<!ENTITY restore.minVersion.warning "Warning: the file has been created with a newer @ADDON_NAME@ version. You should update to the latest @ADDON_NAME@ version before restoring from this file.">
|
||||
|
||||
<!ENTITY find.label "Find">
|
||||
<!ENTITY find.accesskey "n">
|
||||
|
||||
<!ENTITY close.label "Close">
|
||||
@@ -0,0 +1,27 @@
|
||||
#filter substitution
|
||||
|
||||
<!ENTITY dialog.title "@ADDON_NAME@ installation complete">
|
||||
|
||||
<!ENTITY confirmation "
|
||||
Thank you for installing @ADDON_NAME@. Ads will be blocked from now on.
|
||||
Enjoy!">
|
||||
|
||||
<!ENTITY advancedSection "Advanced options">
|
||||
|
||||
<!ENTITY listSelection1 "
|
||||
The following filter list has been configured to block advertising:
|
||||
">
|
||||
|
||||
<!ENTITY noList "No filter list">
|
||||
<!ENTITY visitHomepage.label "Visit list home page">
|
||||
|
||||
<!ENTITY acceptableAds2 "
|
||||
@ADDON_NAME@ has also been configured to allow some non-intrusive advertising.
|
||||
">
|
||||
|
||||
<!ENTITY viewList.label "View list">
|
||||
<!ENTITY readMore.label "Read more about this">
|
||||
|
||||
<!ENTITY listSelection2 "
|
||||
You can change this selection at any time in the [link]Filter Preferences[/link].
|
||||
">
|
||||
@@ -0,0 +1,60 @@
|
||||
#filter substitution
|
||||
|
||||
default_dialog_title=@ADDON_NAME@
|
||||
|
||||
action0_tooltip=Click to bring up context menu, middle-click to enable/disable.
|
||||
action1_tooltip=Click to open/close blockable items, middle-click to enable/disable.
|
||||
action2_tooltip=Click to open preferences, middle-click to enable/disable.
|
||||
action3_tooltip=Click to enable/disable @ADDON_NAME@.
|
||||
|
||||
disabled_tooltip=@ADDON_NAME@ is disabled.
|
||||
# Note: the placeholder ?1? will be replaced by the number of active filter subscriptions, the placeholder ?2? by the number of custom filters
|
||||
active_tooltip=@ADDON_NAME@ is enabled, ?1? filter subscription(s) and ?2? custom filter(s) in use.
|
||||
whitelisted_tooltip=@ADDON_NAME@ is disabled on current page.
|
||||
|
||||
# Note: the placeholder ?1? will be replaced by the number of blocked items, the placeholder ?2? by the total number of items on current page
|
||||
blocked_count_tooltip=?1? out of ?2?
|
||||
# Note: the placeholder ?1? will be replaced by the number of whitelisted items, the placeholder ?2? by the number of hidden items on current page
|
||||
blocked_count_addendum=(also whitelisted: ?1?, hidden: ?2?)
|
||||
|
||||
no_blocking_suggestions=No blockable items on the current page
|
||||
whitelisted_page=@ADDON_NAME@ has been disabled for the current page
|
||||
|
||||
newGroup_title=New filter group
|
||||
whitelistGroup_title=Exception Rules
|
||||
blockingGroup_title=Ad Blocking Rules
|
||||
elemhideGroup_title=Element Hiding Rules
|
||||
|
||||
remove_subscription_warning=Do you really want to remove this subscription?
|
||||
remove_group_warning=Do you really want to remove this group?
|
||||
clearStats_warning=This will reset all filter hit statistics and disable counting filter hits. Do you want to proceed?
|
||||
|
||||
filter_regexp_tooltip=This filter is either a regular expression or too short to be optimized. Too many of these filters might slow down your browsing.
|
||||
filter_elemhide_duplicate_id=Only one ID of the element to be hidden can be specified
|
||||
filter_elemhide_nocriteria=No criteria specified to recognize the element to be hidden
|
||||
|
||||
subscription_invalid_location=Filter list location is neither a valid URL nor a valid file name.
|
||||
|
||||
type_label_other=other
|
||||
type_label_script=script
|
||||
type_label_image=image
|
||||
type_label_stylesheet=stylesheet
|
||||
type_label_object=object
|
||||
type_label_subdocument=frame
|
||||
type_label_document=document
|
||||
type_label_elemhide=hidden
|
||||
type_label_popup=pop-up window
|
||||
type_label_websocket=websocket
|
||||
type_label_webrtc=webtrc
|
||||
type_label_csp=csp
|
||||
|
||||
type_label_xmlhttprequest=XML request
|
||||
type_label_object_subrequest=object subrequest
|
||||
type_label_media=audio/video
|
||||
type_label_font=font
|
||||
|
||||
mobile_menu_enable=ABP: Enable
|
||||
# Note: the placeholder ?1? will be replaced by the site name. Ideally it should be at the end of the string (space is limited and site names can be long).
|
||||
mobile_menu_enable_site=ABP: Enable on ?1?
|
||||
# Note: the placeholder ?1? will be replaced by the site name. Ideally it should be at the end of the string (space is limited and site names can be long).
|
||||
mobile_menu_disable_site=ABP: Disable on ?1?
|
||||
@@ -0,0 +1,17 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#filter substitution
|
||||
|
||||
[.] chrome.jar:
|
||||
% locale @ADDON_CHROME_NAME@ en-US chrome/locale/
|
||||
* locale/about.dtd
|
||||
* locale/composer.dtd
|
||||
* locale/filters.dtd
|
||||
* locale/firstRun.dtd
|
||||
* locale/global.properties
|
||||
* locale/overlay.dtd
|
||||
* locale/sendReport.dtd
|
||||
* locale/sidebar.dtd
|
||||
* locale/subscriptionSelection.dtd
|
||||
@@ -0,0 +1,6 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
@@ -0,0 +1,54 @@
|
||||
#filter substitution
|
||||
|
||||
<!ENTITY status.tooltip "Status:">
|
||||
<!ENTITY blocked.tooltip "Blocked items on this page:">
|
||||
<!ENTITY filters.tooltip "Most active filters:">
|
||||
|
||||
<!ENTITY menuitem.label "@ADDON_NAME@ Preferences">
|
||||
<!ENTITY menuitem.accesskey "b">
|
||||
<!ENTITY toolbarbutton.label "@ADDON_NAME@">
|
||||
<!ENTITY toolsmenu.label "@ADDON_NAME@">
|
||||
|
||||
<!ENTITY context.image.label "@ADDON_NAME@: Block image">
|
||||
<!ENTITY context.object.label "@ADDON_NAME@: Block object">
|
||||
<!ENTITY context.frame.label "@ADDON_NAME@: Block frame">
|
||||
<!ENTITY context.media.label "@ADDON_NAME@: Block audio/video">
|
||||
<!ENTITY context.removeWhitelist.label "@ADDON_NAME@: Re-enable on this page">
|
||||
|
||||
<!ENTITY sidebar.title "Blockable items on current page">
|
||||
|
||||
<!ENTITY sendReport.label "Report issue on this page">
|
||||
<!ENTITY sendReport.accesskey "R">
|
||||
<!ENTITY filters.label "Filter preferences">
|
||||
<!ENTITY filters.accesskey "F">
|
||||
<!ENTITY opensidebar.label "Open blockable items">
|
||||
<!-- Note: This access key should usually be the same as closesidebar.accesskey. It is the same menu item with different label depending on whether the sidebar is currently open.-->
|
||||
<!ENTITY opensidebar.accesskey "b">
|
||||
<!ENTITY closesidebar.label "Close blockable items">
|
||||
<!-- Note: This access key should usually be the same as opensidebar.accesskey. It is the same menu item with different label depending on whether the sidebar is currently open.-->
|
||||
<!ENTITY closesidebar.accesskey "b">
|
||||
<!-- Note: the placeholder ?1? will be replaced by the domain name of the current page -->
|
||||
<!ENTITY whitelist.site.label "Disable on ?1?">
|
||||
<!ENTITY whitelist.page.label "Disable on this page only">
|
||||
<!ENTITY disable.label "Disable everywhere">
|
||||
<!ENTITY options.label "Options">
|
||||
<!ENTITY options.accesskey "O">
|
||||
<!ENTITY contribute.label "Contribute to @ADDON_NAME@">
|
||||
|
||||
<!ENTITY showintoolbar.label "Show in toolbar">
|
||||
<!ENTITY showintoolbar.accesskey "b">
|
||||
<!ENTITY showinstatusbar.label "Show in status bar">
|
||||
<!ENTITY showinstatusbar.accesskey "s">
|
||||
<!ENTITY showinaddonbar.label "Show in add-on bar">
|
||||
<!ENTITY showinaddonbar.accesskey "b">
|
||||
<!ENTITY objecttabs.label "Show tabs on Flash and Java">
|
||||
<!ENTITY objecttabs.accesskey "t">
|
||||
<!ENTITY hideplaceholders.label "Hide placeholders of blocked elements">
|
||||
<!ENTITY hideplaceholders.accesskey "l">
|
||||
<!ENTITY counthits.label "Count filter hits">
|
||||
<!ENTITY counthits.accesskey "h">
|
||||
<!ENTITY sync.label "Sync @ADDON_NAME@ settings">
|
||||
<!ENTITY sync.accesskey "c">
|
||||
|
||||
<!ENTITY objecttab.title "Block">
|
||||
<!ENTITY objecttab.tooltip "Click here to block this object with @ADDON_NAME@">
|
||||
@@ -0,0 +1,201 @@
|
||||
#filter substitution
|
||||
|
||||
<!ENTITY wizard.title "Issue reporter">
|
||||
<!ENTITY privacyPolicy.label "Privacy policy">
|
||||
|
||||
<!ENTITY dataCollector.heading "Welcome to the issue reporter">
|
||||
<!ENTITY dataCollector.description "Please wait a few moments while @ADDON_NAME@ gathers the required data.">
|
||||
|
||||
<!-- Please keep typeSelector.heading short - it is shown in the progress bar, not much space there -->
|
||||
<!ENTITY typeSelector.heading "Select issue type">
|
||||
|
||||
<!ENTITY typeSelector.description "
|
||||
This window will guide you through the steps required for the submission of an Adblock
|
||||
Latitude issue report. First, please select the type of issue that you are experiencing
|
||||
on this page:
|
||||
">
|
||||
|
||||
<!ENTITY typeSelector.falsePositive.label "@ADDON_NAME@ is blocking too much">
|
||||
<!ENTITY typeSelector.falsePositive.accesskey "m">
|
||||
<!ENTITY typeSelector.falsePositive.description "
|
||||
Select this option if the page lacks important content, displays incorrectly or
|
||||
fails to function properly. You can determine whether @ADDON_NAME@ is the cause
|
||||
of the problem by disabling it temporarily.
|
||||
">
|
||||
|
||||
<!ENTITY typeSelector.falseNegative.label "@ADDON_NAME@ doesn't block an advertisement">
|
||||
<!ENTITY typeSelector.falseNegative.accesskey "v">
|
||||
<!ENTITY typeSelector.falseNegative.description "
|
||||
Select this option if an advertisement is displayed despite
|
||||
@ADDON_NAME@ being enabled.
|
||||
">
|
||||
|
||||
<!ENTITY typeSelector.other.label "Other issue">
|
||||
<!ENTITY typeSelector.other.accesskey "t">
|
||||
<!ENTITY typeSelector.other.description "
|
||||
Select this option if you suspect an issue with @ADDON_NAME@ itself rather
|
||||
than its filters.
|
||||
">
|
||||
|
||||
<!ENTITY showRecentReports.label "Show recently submitted reports">
|
||||
<!ENTITY recentReports.label "Your recently submitted reports">
|
||||
<!ENTITY recentReports.clear.label "Remove all reports">
|
||||
<!ENTITY recentReports.clear.accesskey "R">
|
||||
|
||||
<!ENTITY update.inProgress.description "
|
||||
@ADDON_NAME@ needs to update your filter subscriptions to make sure that the
|
||||
issue hasn't been resolved already. Please wait...
|
||||
">
|
||||
|
||||
<!ENTITY update.fixed.description "
|
||||
The updates to your filter subscriptions likely resolved the issue that you
|
||||
were reporting. Please reload the page and retry, hit Report again if the
|
||||
problem remains.
|
||||
">
|
||||
|
||||
<!ENTITY outdatedSubscriptions.description "
|
||||
The following filter subscriptions haven't been updated for at least two
|
||||
weeks. Please update these subscriptions before submitting a report, the
|
||||
issue might be resolved already.
|
||||
">
|
||||
|
||||
<!ENTITY update.start.label "Start update now">
|
||||
|
||||
<!ENTITY issues.description "
|
||||
@ADDON_NAME@ has detected issues with your configuration that might be responsible
|
||||
for this issue or will make investigating the report difficult.
|
||||
">
|
||||
|
||||
<!ENTITY issues.whitelist.description "
|
||||
@ADDON_NAME@ is currently disabled on the page you are reporting. Please re-enable
|
||||
it and reload the page before submitting the report to assist the investigation of
|
||||
this issue.
|
||||
">
|
||||
<!ENTITY issues.whitelist.remove.label "Re-enable @ADDON_NAME@ on this page">
|
||||
|
||||
<!ENTITY issues.disabled.description "
|
||||
@ADDON_NAME@ is disabled, it will not block anything in its current state.
|
||||
">
|
||||
<!ENTITY issues.disabled.enable.label "Enable @ADDON_NAME@">
|
||||
|
||||
<!ENTITY issues.nofilters.description "
|
||||
@ADDON_NAME@ isn't blocking anything on the current page. The issue you are
|
||||
observing is most likely unrelated to @ADDON_NAME@.
|
||||
">
|
||||
|
||||
<!ENTITY issues.nosubscriptions.description "
|
||||
You do not appear to be subscribed to any of the pre-made filter lists that
|
||||
automatically remove unwanted content from websites.
|
||||
">
|
||||
<!ENTITY issues.nosubscriptions.add.label "Add filter subscription">
|
||||
|
||||
<!ENTITY issues.subscriptionCount.description "
|
||||
It seems that you are subscribed to too many filter subscriptions. This
|
||||
setup is not recommended because it will make the likeliness
|
||||
of issues much higher. We also cannot accept your issue report because it
|
||||
is unclear which filter subscription author needs to take action. Please
|
||||
remove all but the really necessary filter subscriptions and test whether
|
||||
the issue still occurs then.
|
||||
">
|
||||
<!ENTITY issues.openPreferences.label "Open filter preferences">
|
||||
|
||||
<!ENTITY issues.ownfilters.description "
|
||||
Some of the filters applied on this page are user-defined. Please disable
|
||||
the filters that might have caused the issue:
|
||||
">
|
||||
<!ENTITY issues.ownfilters.disable.label "Disable filter">
|
||||
|
||||
<!ENTITY issues.disabledgroups.description "
|
||||
The following filter subscriptions / filter groups are disabled, yet they might have
|
||||
an effect on this page:
|
||||
">
|
||||
<!ENTITY issues.disabledgroups.enable.label "Enable filter subscription / filter group">
|
||||
|
||||
<!ENTITY issues.disabledfilters.description "
|
||||
The following filters are disabled, yet they might have an effect on this page:
|
||||
">
|
||||
<!ENTITY issues.disabledfilters.enable.label "Enable filter">
|
||||
|
||||
<!ENTITY issues.override.label "The configuration is correct, continue with the report">
|
||||
<!ENTITY issues.override.accesskey "c">
|
||||
<!ENTITY issues.change.description "
|
||||
Your configuration has been changed. Please reload the page to test the changes
|
||||
and submit a report if the issue hasn't been resolved by the alterations.
|
||||
">
|
||||
|
||||
<!ENTITY typeWarning.description "
|
||||
You have indicated that you want to report a general issue with @ADDON_NAME@ rather
|
||||
than a problem with the filters. Please note that such issues are best reported
|
||||
in the [link]@ADDON_NAME@ forum[/link]. You should only use the issue reporter to
|
||||
supplement an existing discussion, as nobody will notice your report
|
||||
unless you provide them with the link to it. The automatically generated link
|
||||
will be provided after submitting the report.
|
||||
">
|
||||
|
||||
<!ENTITY typeWarning.override.label "I understand and want to submit the report anyway">
|
||||
<!ENTITY typeWarning.override.accesskey "s">
|
||||
|
||||
<!ENTITY reloadButton.label "Reload page">
|
||||
<!ENTITY reloadButton.accesskey "R">
|
||||
|
||||
<!-- Please keep screenshot.heading short - it is shown in the progress bar, not much space there -->
|
||||
<!ENTITY screenshot.heading "Attach screenshot">
|
||||
|
||||
<!ENTITY screenshot.description "
|
||||
The same page can look different for different people. It may help us to
|
||||
understand the problem if you attach a screenshot to your report. You can remove
|
||||
sections containing sensitive information as well as mark areas where the
|
||||
problem is noticeable. To do that click the corresponding button and select
|
||||
a section of the image with your mouse.
|
||||
">
|
||||
|
||||
<!ENTITY screenshot.attach.label "Attach a page image to the report">
|
||||
<!ENTITY screenshot.attach.accesskey "t">
|
||||
<!ENTITY screenshot.mark.label "Mark the problem">
|
||||
<!ENTITY screenshot.mark.accesskey "M">
|
||||
<!ENTITY screenshot.remove.label "Remove sensitive data">
|
||||
<!ENTITY screenshot.remove.accesskey "R">
|
||||
<!ENTITY screenshot.undo.label "Undo">
|
||||
<!ENTITY screenshot.undo.accesskey "U">
|
||||
|
||||
<!-- Please keep commentPage.heading short - it is shown in the progress bar, not much space there -->
|
||||
<!ENTITY commentPage.heading "Enter comment">
|
||||
|
||||
<!ENTITY commentPage.description "
|
||||
The text field below allows you to enter a comment to help us understand the issue.
|
||||
This step is optional but recommended if the problem isn't obvious.
|
||||
You can also review the report data before it is sent.
|
||||
">
|
||||
|
||||
<!ENTITY comment.label "Comment (optional):">
|
||||
<!ENTITY comment.accesskey "C">
|
||||
<!ENTITY comment.lengthWarning "The length of your comment exceeds 1000 characters. Only the first 1000 characters will be sent.">
|
||||
<!ENTITY email.label "Email for further inquiries (optional):">
|
||||
<!ENTITY email.accesskey "m">
|
||||
|
||||
<!ENTITY attachExtensions.label "Attach a list of active extensions to the report in case add-on conflict is the cause of the problem">
|
||||
<!ENTITY attachExtensions.accesskey "x">
|
||||
|
||||
<!ENTITY sendButton.label "Send report">
|
||||
<!ENTITY sendButton.accesskey "n">
|
||||
|
||||
<!ENTITY showData.label "Show report data">
|
||||
<!ENTITY data.label "Report data:">
|
||||
<!ENTITY data.accesskey "p">
|
||||
|
||||
<!-- Please keep sendPage.heading short - it is shown in the progress bar, not much space there -->
|
||||
<!ENTITY sendPage.heading "Send report">
|
||||
<!ENTITY sendPage.waitMessage "Please wait while @ADDON_NAME@ is submitting your report.">
|
||||
<!ENTITY sendPage.confirmation "Your report has been saved. You can access it at the following address:">
|
||||
<!ENTITY sendPage.knownIssue "The issue you reported is probably already known. More information:">
|
||||
|
||||
<!-- Note: the placeholder ?1? will be replaced by the error code -->
|
||||
<!ENTITY sendPage.errorMessage "
|
||||
An attempt to send the report failed with error code "?1?". Please ensure you are
|
||||
connected to the Internet and retry. If the problem persists please request
|
||||
assistance in the [link]@ADDON_NAME@ forum[/link].
|
||||
">
|
||||
<!ENTITY sendPage.retry.label "Send again">
|
||||
|
||||
<!ENTITY copyLink.label "Copy report link">
|
||||
<!ENTITY copyLink.accesskey "C">
|
||||
@@ -0,0 +1,44 @@
|
||||
#filter substitution
|
||||
|
||||
<!ENTITY detached.title "@ADDON_NAME@: Blockable items (detached)">
|
||||
|
||||
<!ENTITY detach.label "Detach">
|
||||
<!ENTITY reattach.label "Reattach">
|
||||
|
||||
<!ENTITY search.label "Search:">
|
||||
<!ENTITY search.accesskey "S">
|
||||
<!ENTITY type.label "Type">
|
||||
<!ENTITY address.label "Address">
|
||||
<!ENTITY filter.label "Filter">
|
||||
<!ENTITY state.label "State">
|
||||
<!ENTITY size.label "Size">
|
||||
<!ENTITY filterSource.label "Filter source">
|
||||
<!ENTITY docDomain.label "Document source">
|
||||
<!ENTITY docDomain.thirdParty "(third party)">
|
||||
<!ENTITY docDomain.firstParty "(first party)">
|
||||
<!ENTITY noitems.label "No blockable items">
|
||||
<!ENTITY whitelisted.label "Whitelisted page">
|
||||
<!ENTITY tooltip.address.label "Address:">
|
||||
<!ENTITY tooltip.type.label "Type:">
|
||||
<!ENTITY tooltip.type.blocked "(blocked)">
|
||||
<!ENTITY tooltip.type.whitelisted "(whitelisted)">
|
||||
<!ENTITY tooltip.size.label "Size:">
|
||||
<!ENTITY tooltip.docDomain.label "Document source:">
|
||||
<!ENTITY tooltip.filter.label "Filter in effect:">
|
||||
<!ENTITY tooltip.filter.disabled "(disabled)">
|
||||
<!ENTITY tooltip.filterSource.label "Filter source:">
|
||||
|
||||
<!ENTITY context.block.label "Block this item">
|
||||
<!ENTITY context.editfilter.label "Edit filter in effect">
|
||||
<!ENTITY context.whitelist.label "Add exception rule for item">
|
||||
<!-- Note: the placeholder ?1? will be replaced by the filter text -->
|
||||
<!ENTITY context.disablefilter.label "Disable filter ?1?">
|
||||
<!-- Note: the placeholder ?1? will be replaced by the filter text -->
|
||||
<!ENTITY context.enablefilter.label "Re-enable filter ?1?">
|
||||
<!-- Note: the placeholder ?1? will be replaced by the domain name of the current page -->
|
||||
<!ENTITY context.disablefilteronsite.label "Disable this filter on ?1?">
|
||||
<!ENTITY context.open.label "Open in New Tab">
|
||||
<!ENTITY context.flash.label "Flash item's borders">
|
||||
<!ENTITY context.copy.label "Copy item's address">
|
||||
<!ENTITY context.copyFilter.label "Copy filter">
|
||||
<!ENTITY context.selectAll.label "Select all">
|
||||
@@ -0,0 +1,27 @@
|
||||
#filter substitution
|
||||
|
||||
<!ENTITY dialog.title "Add @ADDON_NAME@ filter subscription">
|
||||
|
||||
<!ENTITY subscriptionSelector.label "Please choose a filter subscription from the list:">
|
||||
|
||||
<!ENTITY viewList.label "View filters">
|
||||
<!ENTITY visitHomepage.label "Visit home page">
|
||||
|
||||
<!ENTITY addSubscription.label "Add subscription">
|
||||
|
||||
<!ENTITY list.download.failed "@ADDON_NAME@ failed to retrieve the list of subscriptions.">
|
||||
<!ENTITY list.download.retry "Try again">
|
||||
<!ENTITY list.download.website "View website">
|
||||
|
||||
<!ENTITY fromWeb.description "Please confirm that you want to add this filter subscription. You can change the subscription title or location before adding it.">
|
||||
|
||||
<!ENTITY title.label "Subscription title:">
|
||||
<!ENTITY title.accesskey "t">
|
||||
<!ENTITY location.label "Filter list location:">
|
||||
<!ENTITY location.accesskey "l">
|
||||
|
||||
<!-- Note: the placeholder (?1?) will be replaced by the name of the filter subscription required -->
|
||||
<!ENTITY supplementMessage "This filter subscription is meant to be used with the filter subscription "?1?" which you are not using yet.">
|
||||
<!-- Note: the placeholder (?1?) will be replaced by the name of the filter subscription required -->
|
||||
<!ENTITY addMain.label "Add filter subscription "?1?" as well">
|
||||
<!ENTITY addMain.accesskey "s">
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Bootstrap module, will initialize Adblock Plus when loaded
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Bootstrap"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
Cu.import(baseURL + "TimeLine.jsm");
|
||||
|
||||
let publicURL = Services.io.newURI(baseURL + "Public.jsm", null, null);
|
||||
if (publicURL instanceof Ci.nsIMutable)
|
||||
publicURL.mutable = false;
|
||||
|
||||
const cidPublic = Components.ID("5e447bce-1dd2-11b2-b151-ec21c2b6a135");
|
||||
const contractIDPublic = "@adblockplus.org/abp/public;1";
|
||||
let factoryPublic =
|
||||
{
|
||||
createInstance: function(outer, iid)
|
||||
{
|
||||
if (outer)
|
||||
throw Cr.NS_ERROR_NO_AGGREGATION;
|
||||
return publicURL.QueryInterface(iid);
|
||||
}
|
||||
};
|
||||
|
||||
let defaultModules = [
|
||||
baseURL + "Prefs.jsm",
|
||||
baseURL + "FilterListener.jsm",
|
||||
baseURL + "ContentPolicy.jsm",
|
||||
baseURL + "Synchronizer.jsm",
|
||||
// baseURL + "Sync.jsm"
|
||||
];
|
||||
|
||||
let loadedModules = {__proto__: null};
|
||||
|
||||
let initialized = false;
|
||||
|
||||
/**
|
||||
* Allows starting up and shutting down Adblock Plus functions.
|
||||
* @class
|
||||
*/
|
||||
var Bootstrap =
|
||||
{
|
||||
/**
|
||||
* Initializes add-on, loads and initializes all modules.
|
||||
*/
|
||||
startup: function()
|
||||
{
|
||||
if (initialized)
|
||||
return;
|
||||
initialized = true;
|
||||
|
||||
TimeLine.enter("Entered Bootstrap.startup()");
|
||||
|
||||
// Register component to allow retrieving public URL
|
||||
|
||||
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
registrar.registerFactory(cidPublic, "Adblock Plus public module URL", contractIDPublic, factoryPublic);
|
||||
|
||||
TimeLine.log("done registering URL components");
|
||||
|
||||
// Load and initialize modules
|
||||
|
||||
TimeLine.log("started initializing modules");
|
||||
|
||||
for each (let url in defaultModules)
|
||||
Bootstrap.loadModule(url);
|
||||
|
||||
TimeLine.leave("Bootstrap.startup() done");
|
||||
},
|
||||
|
||||
/**
|
||||
* Shuts down add-on.
|
||||
*/
|
||||
shutdown: function()
|
||||
{
|
||||
if (!initialized)
|
||||
return;
|
||||
|
||||
TimeLine.enter("Entered Bootstrap.shutdown()");
|
||||
|
||||
// Shut down modules
|
||||
for (let url in loadedModules)
|
||||
Bootstrap.shutdownModule(url);
|
||||
|
||||
TimeLine.leave("Bootstrap.shutdown() done");
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads and initializes a module.
|
||||
*/
|
||||
loadModule: function(/**String*/ url)
|
||||
{
|
||||
if (url in loadedModules)
|
||||
return;
|
||||
|
||||
let module = {};
|
||||
try
|
||||
{
|
||||
Cu.import(url, module);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError("Adblock Plus: Failed to load module " + url + ": " + e);
|
||||
return;
|
||||
}
|
||||
|
||||
for each (let obj in module)
|
||||
{
|
||||
if ("startup" in obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
obj.startup();
|
||||
loadedModules[url] = obj;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError("Adblock Plus: Calling method startup() for module " + url + " failed: " + e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Cu.reportError("Adblock Plus: No exported object with startup() method found for module " + url);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shuts down a module.
|
||||
*/
|
||||
shutdownModule: function(/**String*/ url)
|
||||
{
|
||||
if (!(url in loadedModules))
|
||||
return;
|
||||
|
||||
let obj = loadedModules[url];
|
||||
if ("shutdown" in obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
obj.shutdown();
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError("Adblock Plus: Calling method shutdown() for module " + url + " failed: " + e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,634 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Content policy implementation, responsible for blocking things.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Policy"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import(baseURL + "TimeLine.jsm");
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
Cu.import(baseURL + "Prefs.jsm");
|
||||
Cu.import(baseURL + "FilterStorage.jsm");
|
||||
Cu.import(baseURL + "FilterClasses.jsm");
|
||||
Cu.import(baseURL + "Matcher.jsm");
|
||||
Cu.import(baseURL + "ObjectTabs.jsm");
|
||||
Cu.import(baseURL + "RequestNotifier.jsm");
|
||||
|
||||
/**
|
||||
* List of explicitly supported content types
|
||||
* @type Array of String
|
||||
*/
|
||||
const contentTypes = ["OTHER", "SCRIPT", "IMAGE", "STYLESHEET", "OBJECT", "SUBDOCUMENT", "DOCUMENT", "XMLHTTPREQUEST", "OBJECT_SUBREQUEST", "FONT", "MEDIA", "WEBSOCKET", "WEBRTC", "CSP"];
|
||||
|
||||
/**
|
||||
* List of content types that aren't associated with a visual document area
|
||||
* @type Array of String
|
||||
*/
|
||||
const nonVisualTypes = ["SCRIPT", "STYLESHEET", "XMLHTTPREQUEST", "OBJECT_SUBREQUEST", "FONT", "WEBSOCKET", "WEBRTC", "CSP"];
|
||||
|
||||
/**
|
||||
* Public policy checking functions and auxiliary objects
|
||||
* @class
|
||||
*/
|
||||
var Policy =
|
||||
{
|
||||
/**
|
||||
* Map of content type identifiers by their name.
|
||||
* @type Object
|
||||
*/
|
||||
type: {},
|
||||
|
||||
/**
|
||||
* Map of content type names by their identifiers (reverse of type map).
|
||||
* @type Object
|
||||
*/
|
||||
typeDescr: {},
|
||||
|
||||
/**
|
||||
* Map of localized content type names by their identifiers.
|
||||
* @type Object
|
||||
*/
|
||||
localizedDescr: {},
|
||||
|
||||
/**
|
||||
* Lists the non-visual content types.
|
||||
* @type Object
|
||||
*/
|
||||
nonVisual: {},
|
||||
|
||||
/**
|
||||
* Map containing all schemes that should be ignored by content policy.
|
||||
* @type Object
|
||||
*/
|
||||
whitelistSchemes: {},
|
||||
|
||||
/**
|
||||
* Called on module startup.
|
||||
*/
|
||||
startup: function()
|
||||
{
|
||||
TimeLine.enter("Entered ContentPolicy.startup()");
|
||||
|
||||
// type constant by type description and type description by type constant
|
||||
var iface = Ci.nsIContentPolicy;
|
||||
for each (let typeName in contentTypes)
|
||||
{
|
||||
if ("TYPE_" + typeName in iface)
|
||||
{
|
||||
let id = iface["TYPE_" + typeName];
|
||||
Policy.type[typeName] = id;
|
||||
Policy.typeDescr[id] = typeName;
|
||||
Policy.localizedDescr[id] = Utils.getString("type_label_" + typeName.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
Policy.type.ELEMHIDE = 0xFFFD;
|
||||
Policy.typeDescr[0xFFFD] = "ELEMHIDE";
|
||||
Policy.localizedDescr[0xFFFD] = Utils.getString("type_label_elemhide");
|
||||
|
||||
Policy.type.POPUP = 0xFFFE;
|
||||
Policy.typeDescr[0xFFFE] = "POPUP";
|
||||
Policy.localizedDescr[0xFFFE] = Utils.getString("type_label_popup");
|
||||
|
||||
for each (let type in nonVisualTypes)
|
||||
Policy.nonVisual[Policy.type[type]] = true;
|
||||
|
||||
// whitelisted URL schemes
|
||||
for each (var scheme in Prefs.whitelistschemes.toLowerCase().split(" "))
|
||||
Policy.whitelistSchemes[scheme] = true;
|
||||
|
||||
TimeLine.log("done initializing types");
|
||||
|
||||
// Generate class identifier used to collapse node and register corresponding
|
||||
// stylesheet.
|
||||
TimeLine.log("registering global stylesheet");
|
||||
|
||||
let offset = "a".charCodeAt(0);
|
||||
Utils.collapsedClass = "";
|
||||
for (let i = 0; i < 20; i++)
|
||||
Utils.collapsedClass += String.fromCharCode(offset + Math.random() * 26);
|
||||
|
||||
let collapseStyle = Utils.makeURI("data:text/css," +
|
||||
encodeURIComponent("." + Utils.collapsedClass +
|
||||
"{-moz-binding: url(chrome://global/content/bindings/general.xml#foobarbazdummy) !important;}"));
|
||||
Utils.styleService.loadAndRegisterSheet(collapseStyle, Ci.nsIStyleSheetService.USER_SHEET);
|
||||
TimeLine.log("done registering stylesheet");
|
||||
|
||||
// Register our content policy
|
||||
TimeLine.log("registering component");
|
||||
|
||||
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
try
|
||||
{
|
||||
registrar.registerFactory(PolicyPrivate.classID, PolicyPrivate.classDescription, PolicyPrivate.contractID, PolicyPrivate);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Don't stop on errors - the factory might already be registered
|
||||
Cu.reportError(e);
|
||||
}
|
||||
|
||||
let catMan = Utils.categoryManager;
|
||||
for each (let category in PolicyPrivate.xpcom_categories)
|
||||
catMan.addCategoryEntry(category, PolicyPrivate.classDescription, PolicyPrivate.contractID, false, true);
|
||||
|
||||
Services.obs.addObserver(PolicyPrivate, "http-on-modify-request", true);
|
||||
Services.obs.addObserver(PolicyPrivate, "content-document-global-created", true);
|
||||
|
||||
TimeLine.leave("ContentPolicy.startup() done");
|
||||
},
|
||||
|
||||
shutdown: function()
|
||||
{
|
||||
PolicyPrivate.previousRequest = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether a node should be blocked, hides it if necessary
|
||||
* @param wnd {nsIDOMWindow}
|
||||
* @param node {nsIDOMElement}
|
||||
* @param contentType {String}
|
||||
* @param location {nsIURI}
|
||||
* @param collapse {Boolean} true to force hiding of the node
|
||||
* @return {Boolean} false if the node should be blocked
|
||||
*/
|
||||
processNode: function(wnd, node, contentType, location, collapse)
|
||||
{
|
||||
let topWnd = wnd.top;
|
||||
if (!topWnd || !topWnd.location || !topWnd.location.href)
|
||||
return true;
|
||||
|
||||
let originWindow = Utils.getOriginWindow(wnd);
|
||||
let wndLocation = originWindow.location.href;
|
||||
let docDomain = getHostname(wndLocation);
|
||||
let match = null;
|
||||
if (!match && Prefs.enabled)
|
||||
{
|
||||
let testWnd = wnd;
|
||||
let parentWndLocation = getWindowLocation(testWnd);
|
||||
while (true)
|
||||
{
|
||||
let testWndLocation = parentWndLocation;
|
||||
parentWndLocation = (testWnd == testWnd.parent ? testWndLocation : getWindowLocation(testWnd.parent));
|
||||
match = Policy.isWhitelisted(testWndLocation, parentWndLocation);
|
||||
|
||||
if (!(match instanceof WhitelistFilter))
|
||||
|
||||
if (match instanceof WhitelistFilter)
|
||||
{
|
||||
FilterStorage.increaseHitCount(match);
|
||||
RequestNotifier.addNodeData(testWnd.document, topWnd, Policy.type.DOCUMENT, getHostname(parentWndLocation), false, testWndLocation, match);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (testWnd.parent == testWnd)
|
||||
break;
|
||||
else
|
||||
testWnd = testWnd.parent;
|
||||
}
|
||||
}
|
||||
|
||||
// Data loaded by plugins should be attached to the document
|
||||
if (contentType == Policy.type.OBJECT_SUBREQUEST && node instanceof Ci.nsIDOMElement)
|
||||
node = node.ownerDocument;
|
||||
|
||||
// Fix type for objects misrepresented as frames or images
|
||||
if (contentType != Policy.type.OBJECT && (node instanceof Ci.nsIDOMHTMLObjectElement || node instanceof Ci.nsIDOMHTMLEmbedElement))
|
||||
contentType = Policy.type.OBJECT;
|
||||
|
||||
let locationText = location.spec;
|
||||
if (!match && contentType == Policy.type.ELEMHIDE)
|
||||
{
|
||||
let testWnd = wnd;
|
||||
let parentWndLocation = getWindowLocation(testWnd);
|
||||
while (true)
|
||||
{
|
||||
let testWndLocation = parentWndLocation;
|
||||
parentWndLocation = (testWnd == testWnd.parent ? testWndLocation : getWindowLocation(testWnd.parent));
|
||||
let parentDocDomain = getHostname(parentWndLocation);
|
||||
match = defaultMatcher.matchesAny(testWndLocation, "ELEMHIDE", parentDocDomain, false);
|
||||
if (match instanceof WhitelistFilter)
|
||||
{
|
||||
FilterStorage.increaseHitCount(match);
|
||||
RequestNotifier.addNodeData(testWnd.document, topWnd, contentType, parentDocDomain, false, testWndLocation, match);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (testWnd.parent == testWnd)
|
||||
break;
|
||||
else
|
||||
testWnd = testWnd.parent;
|
||||
}
|
||||
|
||||
match = location;
|
||||
locationText = match.text.replace(/^.*?#/, '#');
|
||||
location = locationText;
|
||||
|
||||
if (!match.isActiveOnDomain(docDomain))
|
||||
return true;
|
||||
}
|
||||
|
||||
let thirdParty = (contentType == Policy.type.ELEMHIDE ? false : isThirdParty(location, docDomain));
|
||||
|
||||
if (!match && Prefs.enabled)
|
||||
{
|
||||
match = defaultMatcher.matchesAny(locationText, Policy.typeDescr[contentType] || "", docDomain, thirdParty);
|
||||
if (match instanceof BlockingFilter && node.ownerDocument && !(contentType in Policy.nonVisual))
|
||||
{
|
||||
let prefCollapse = (match.collapse != null ? match.collapse : !Prefs.fastcollapse);
|
||||
if (collapse || prefCollapse)
|
||||
Utils.schedulePostProcess(node);
|
||||
}
|
||||
|
||||
// Track mouse events for objects
|
||||
if (!match && contentType == Policy.type.OBJECT && node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE)
|
||||
{
|
||||
node.addEventListener("mouseover", objectMouseEventHander, true);
|
||||
node.addEventListener("mouseout", objectMouseEventHander, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Store node data
|
||||
RequestNotifier.addNodeData(node, topWnd, contentType, docDomain, thirdParty, locationText, match);
|
||||
if (match)
|
||||
FilterStorage.increaseHitCount(match);
|
||||
|
||||
return !match || match instanceof WhitelistFilter;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether the location's scheme is blockable.
|
||||
* @param location {nsIURI}
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isBlockableScheme: function(location)
|
||||
{
|
||||
return !(location.scheme in Policy.whitelistSchemes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether a page is whitelisted.
|
||||
* @param {String} url
|
||||
* @param {String} [parentUrl] location of the parent page
|
||||
* @return {Filter} filter that matched the URL or null if not whitelisted
|
||||
*/
|
||||
isWhitelisted: function(url, parentUrl)
|
||||
{
|
||||
if (!url)
|
||||
return null;
|
||||
|
||||
// Do not apply exception rules to schemes on our whitelistschemes list.
|
||||
let match = /^([\w\-]+):/.exec(url);
|
||||
if (match && match[1] in Policy.whitelistSchemes)
|
||||
return null;
|
||||
|
||||
if (!parentUrl)
|
||||
parentUrl = url;
|
||||
|
||||
// Ignore fragment identifier
|
||||
let index = url.indexOf("#");
|
||||
if (index >= 0)
|
||||
url = url.substring(0, index);
|
||||
|
||||
let result = defaultMatcher.matchesAny(url, "DOCUMENT", getHostname(parentUrl), false);
|
||||
return (result instanceof WhitelistFilter ? result : null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether the page loaded in a window is whitelisted.
|
||||
* @param wnd {nsIDOMWindow}
|
||||
* @return {Filter} matching exception rule or null if not whitelisted
|
||||
*/
|
||||
isWindowWhitelisted: function(wnd)
|
||||
{
|
||||
return Policy.isWhitelisted(getWindowLocation(wnd));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Asynchronously re-checks filters for given nodes.
|
||||
*/
|
||||
refilterNodes: function(/**Node[]*/ nodes, /**RequestEntry*/ entry)
|
||||
{
|
||||
// Ignore nodes that have been blocked already
|
||||
if (entry.filter && !(entry.filter instanceof WhitelistFilter))
|
||||
return;
|
||||
|
||||
for each (let node in nodes)
|
||||
Utils.runAsync(refilterNode, this, node, entry);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Private nsIContentPolicy and nsIChannelEventSink implementation
|
||||
* @class
|
||||
*/
|
||||
var PolicyPrivate =
|
||||
{
|
||||
classDescription: "Adblock Plus content policy",
|
||||
classID: Components.ID("cfeaabe6-1dd1-11b2-a0c6-cb5c268894c9"),
|
||||
contractID: "@adblockplus.org/abp/policy;1",
|
||||
xpcom_categories: ["content-policy", "net-channel-event-sinks"],
|
||||
|
||||
//
|
||||
// nsISupports interface implementation
|
||||
//
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIObserver,
|
||||
Ci.nsIChannelEventSink, Ci.nsIFactory, Ci.nsISupportsWeakReference]),
|
||||
|
||||
//
|
||||
// nsIContentPolicy interface implementation
|
||||
//
|
||||
|
||||
shouldLoad: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra)
|
||||
{
|
||||
// Ignore requests without context and top-level documents
|
||||
if (!node || contentType == Policy.type.DOCUMENT)
|
||||
return Ci.nsIContentPolicy.ACCEPT;
|
||||
|
||||
// Ignore standalone objects
|
||||
if (contentType == Policy.type.OBJECT && node.ownerDocument && !/^text\/|[+\/]xml$/.test(node.ownerDocument.contentType))
|
||||
return Ci.nsIContentPolicy.ACCEPT;
|
||||
|
||||
let wnd = Utils.getWindow(node);
|
||||
if (!wnd)
|
||||
return Ci.nsIContentPolicy.ACCEPT;
|
||||
|
||||
// Ignore whitelisted schemes
|
||||
let location = Utils.unwrapURL(contentLocation);
|
||||
if (!Policy.isBlockableScheme(location))
|
||||
return Ci.nsIContentPolicy.ACCEPT;
|
||||
|
||||
// Interpret unknown types as "other"
|
||||
if (!(contentType in Policy.typeDescr))
|
||||
contentType = Policy.type.OTHER;
|
||||
|
||||
let result = Policy.processNode(wnd, node, contentType, location, false);
|
||||
if (result)
|
||||
{
|
||||
// We didn't block this request so we will probably see it again in
|
||||
// http-on-modify-request. Keep it so that we can associate it with the
|
||||
// channel there - will be needed in case of redirect.
|
||||
PolicyPrivate.previousRequest = [location, contentType];
|
||||
}
|
||||
return (result ? Ci.nsIContentPolicy.ACCEPT : Ci.nsIContentPolicy.REJECT_REQUEST);
|
||||
},
|
||||
|
||||
shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra)
|
||||
{
|
||||
return Ci.nsIContentPolicy.ACCEPT;
|
||||
},
|
||||
|
||||
//
|
||||
// nsIObserver interface implementation
|
||||
//
|
||||
observe: function(subject, topic, data, additional)
|
||||
{
|
||||
switch (topic)
|
||||
{
|
||||
case "content-document-global-created":
|
||||
{
|
||||
if (!(subject instanceof Ci.nsIDOMWindow) || !subject.opener)
|
||||
return;
|
||||
|
||||
let uri = additional || Utils.makeURI(subject.location.href);
|
||||
if (!Policy.processNode(subject.opener, subject.opener.document, Policy.type.POPUP, uri, false))
|
||||
{
|
||||
subject.stop();
|
||||
Utils.runAsync(subject.close, subject);
|
||||
}
|
||||
else if (uri.spec == "about:blank")
|
||||
{
|
||||
// An about:blank pop-up most likely means that a load will be
|
||||
// initiated synchronously. Set a flag for our "http-on-modify-request"
|
||||
// handler.
|
||||
PolicyPrivate.expectingPopupLoad = true;
|
||||
Utils.runAsync(function()
|
||||
{
|
||||
PolicyPrivate.expectingPopupLoad = false;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "http-on-modify-request":
|
||||
{
|
||||
if (!(subject instanceof Ci.nsIHttpChannel))
|
||||
return;
|
||||
|
||||
if (Prefs.enabled)
|
||||
{
|
||||
let match = defaultMatcher.matchesAny(subject.URI.spec, "DONOTTRACK", null, false);
|
||||
if (match && match instanceof BlockingFilter)
|
||||
{
|
||||
FilterStorage.increaseHitCount(match);
|
||||
subject.setRequestHeader("DNT", "1", false);
|
||||
|
||||
// Bug 23845 - Some routers are broken and cannot handle DNT header
|
||||
// following Connection header. Make sure Connection header is last.
|
||||
try
|
||||
{
|
||||
let connection = subject.getRequestHeader("Connection");
|
||||
subject.setRequestHeader("Connection", null, false);
|
||||
subject.setRequestHeader("Connection", connection, false);
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (PolicyPrivate.previousRequest && subject.URI == PolicyPrivate.previousRequest[0] &&
|
||||
subject instanceof Ci.nsIWritablePropertyBag)
|
||||
{
|
||||
// We just handled a content policy call for this request - associate
|
||||
// the data with the channel so that we can find it in case of a redirect.
|
||||
subject.setProperty("abpRequestType", PolicyPrivate.previousRequest[1]);
|
||||
PolicyPrivate.previousRequest = null;
|
||||
}
|
||||
|
||||
if (PolicyPrivate.expectingPopupLoad)
|
||||
{
|
||||
let wnd = Utils.getRequestWindow(subject);
|
||||
if (wnd && wnd.opener && wnd.location.href == "about:blank")
|
||||
PolicyPrivate.observe(wnd, "content-document-global-created", null, subject.URI);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// nsIChannelEventSink interface implementation
|
||||
//
|
||||
|
||||
asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback)
|
||||
{
|
||||
let result = Cr.NS_OK;
|
||||
try
|
||||
{
|
||||
// Try to retrieve previously stored request data from the channel
|
||||
let contentType;
|
||||
if (oldChannel instanceof Ci.nsIWritablePropertyBag)
|
||||
{
|
||||
try
|
||||
{
|
||||
contentType = oldChannel.getProperty("abpRequestType");
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
// No data attached, ignore this redirect
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let newLocation = null;
|
||||
try
|
||||
{
|
||||
newLocation = newChannel.URI;
|
||||
} catch(e2) {}
|
||||
if (!newLocation)
|
||||
return;
|
||||
|
||||
let wnd = Utils.getRequestWindow(newChannel);
|
||||
if (!wnd)
|
||||
return;
|
||||
|
||||
if (!Policy.processNode(wnd, wnd.document, contentType, newLocation, false))
|
||||
result = Cr.NS_BINDING_ABORTED;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// We shouldn't throw exceptions here - this will prevent the redirect.
|
||||
Cu.reportError(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
callback.onRedirectVerifyCallback(result);
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// nsIFactory interface implementation
|
||||
//
|
||||
|
||||
createInstance: function(outer, iid)
|
||||
{
|
||||
if (outer)
|
||||
throw Cr.NS_ERROR_NO_AGGREGATION;
|
||||
return this.QueryInterface(iid);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the hostname from a URL (might return null).
|
||||
*/
|
||||
function getHostname(/**String*/ url) /**String*/
|
||||
{
|
||||
try
|
||||
{
|
||||
return Utils.unwrapURL(url).host;
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the location of a window.
|
||||
* @param wnd {nsIDOMWindow}
|
||||
* @return {String} window location or null on failure
|
||||
*/
|
||||
function getWindowLocation(wnd)
|
||||
{
|
||||
if ("name" in wnd && wnd.name == "messagepane")
|
||||
{
|
||||
// Thunderbird branch
|
||||
try
|
||||
{
|
||||
let mailWnd = wnd.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
|
||||
// Typically we get a wrapped mail window here, need to unwrap
|
||||
try
|
||||
{
|
||||
mailWnd = mailWnd.wrappedJSObject;
|
||||
} catch(e) {}
|
||||
|
||||
if ("currentHeaderData" in mailWnd && "content-base" in mailWnd.currentHeaderData)
|
||||
{
|
||||
return mailWnd.currentHeaderData["content-base"].headerValue;
|
||||
}
|
||||
else if ("currentHeaderData" in mailWnd && "from" in mailWnd.currentHeaderData)
|
||||
{
|
||||
let emailAddress = Utils.headerParser.extractHeaderAddressMailboxes(mailWnd.currentHeaderData.from.headerValue);
|
||||
if (emailAddress)
|
||||
return 'mailto:' + emailAddress.replace(/^[\s"]+/, "").replace(/[\s"]+$/, "").replace(/\s/g, '%20');
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Firefox branch
|
||||
return wnd.location.href;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the location's origin is different from document's origin.
|
||||
*/
|
||||
function isThirdParty(/**nsIURI*/location, /**String*/ docDomain) /**Boolean*/
|
||||
{
|
||||
if (!location || !docDomain)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
return Utils.effectiveTLD.getBaseDomain(location) != Utils.effectiveTLD.getBaseDomainFromHost(docDomain);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// EffectiveTLDService throws on IP addresses, just compare the host name
|
||||
let host = "";
|
||||
try
|
||||
{
|
||||
host = location.host;
|
||||
} catch (e) {}
|
||||
return host != docDomain;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-checks filters on an element.
|
||||
*/
|
||||
function refilterNode(/**Node*/ node, /**RequestEntry*/ entry)
|
||||
{
|
||||
let wnd = Utils.getWindow(node);
|
||||
if (!wnd || wnd.closed)
|
||||
return;
|
||||
|
||||
if (entry.type == Policy.type.OBJECT)
|
||||
{
|
||||
node.removeEventListener("mouseover", objectMouseEventHander, true);
|
||||
node.removeEventListener("mouseout", objectMouseEventHander, true);
|
||||
}
|
||||
Policy.processNode(wnd, node, entry.type, Utils.makeURI(entry.location), true);
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Content policy to be loaded in the content process for a multi-process setup (currently only Fennec)
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["PolicyRemote"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://@ADDON_CHROME_NAME@/modules/Utils.jsm");
|
||||
|
||||
/**
|
||||
* nsIContentPolicy and nsIChannelEventSink implementation
|
||||
* @class
|
||||
*/
|
||||
var PolicyRemote =
|
||||
{
|
||||
classDescription: "Adblock Plus content policy",
|
||||
classID: Components.ID("094560a0-4fed-11e0-b8af-0800200c9a66"),
|
||||
contractID: "@adblockplus.org/abp/policy-remote;1",
|
||||
xpcom_categories: ["content-policy", "net-channel-event-sinks"],
|
||||
|
||||
cache: new Cache(512),
|
||||
|
||||
startup: function()
|
||||
{
|
||||
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
try
|
||||
{
|
||||
registrar.registerFactory(PolicyRemote.classID, PolicyRemote.classDescription, PolicyRemote.contractID, PolicyRemote);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Don't stop on errors - the factory might already be registered
|
||||
Cu.reportError(e);
|
||||
}
|
||||
|
||||
let catMan = Utils.categoryManager;
|
||||
for each (let category in PolicyRemote.xpcom_categories)
|
||||
catMan.addCategoryEntry(category, PolicyRemote.classDescription, PolicyRemote.contractID, false, true);
|
||||
|
||||
Services.obs.addObserver(PolicyRemote, "http-on-modify-request", true);
|
||||
Services.obs.addObserver(PolicyRemote, "content-document-global-created", true);
|
||||
|
||||
// Generate class identifier used to collapse node and register corresponding
|
||||
// stylesheet.
|
||||
let offset = "a".charCodeAt(0);
|
||||
Utils.collapsedClass = "";
|
||||
for (let i = 0; i < 20; i++)
|
||||
Utils.collapsedClass += String.fromCharCode(offset + Math.random() * 26);
|
||||
|
||||
let collapseStyle = Utils.makeURI("data:text/css," +
|
||||
encodeURIComponent("." + Utils.collapsedClass +
|
||||
"{-moz-binding: url(chrome://global/content/bindings/general.xml#foobarbazdummy) !important;}"));
|
||||
Utils.styleService.loadAndRegisterSheet(collapseStyle, Ci.nsIStyleSheetService.USER_SHEET);
|
||||
|
||||
// Get notified if we need to invalidate our matching cache
|
||||
Utils.childMessageManager.addMessageListener("AdblockPlus:Matcher:clearCache", function(message)
|
||||
{
|
||||
PolicyRemote.cache.clear();
|
||||
});
|
||||
},
|
||||
|
||||
//
|
||||
// nsISupports interface implementation
|
||||
//
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIObserver,
|
||||
Ci.nsIChannelEventSink, Ci.nsIFactory, Ci.nsISupportsWeakReference]),
|
||||
|
||||
//
|
||||
// nsIContentPolicy interface implementation
|
||||
//
|
||||
|
||||
shouldLoad: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra)
|
||||
{
|
||||
// Ignore requests without context and top-level documents
|
||||
if (!node || contentType == Ci.nsIContentPolicy.TYPE_DOCUMENT)
|
||||
return Ci.nsIContentPolicy.ACCEPT;
|
||||
|
||||
let wnd = Utils.getWindow(node);
|
||||
if (!wnd)
|
||||
return Ci.nsIContentPolicy.ACCEPT;
|
||||
|
||||
wnd = Utils.getOriginWindow(wnd);
|
||||
|
||||
let locations = [];
|
||||
let testWnd = wnd;
|
||||
while (true)
|
||||
{
|
||||
locations.push(testWnd.location.href);
|
||||
if (testWnd.parent == testWnd)
|
||||
break;
|
||||
else
|
||||
testWnd = testWnd.parent;
|
||||
}
|
||||
|
||||
let key = contentType + " " + contentLocation.spec + " " + locations.join(" ");
|
||||
if (!(key in this.cache.data))
|
||||
{
|
||||
this.cache.add(key, Utils.childMessageManager.sendSyncMessage("AdblockPlus:Policy:shouldLoad", {
|
||||
contentType: contentType,
|
||||
contentLocation: contentLocation.spec,
|
||||
locations: locations})[0]);
|
||||
}
|
||||
|
||||
let result = this.cache.data[key];
|
||||
if (result.value == Ci.nsIContentPolicy.ACCEPT)
|
||||
{
|
||||
// We didn't block this request so we will probably see it again in
|
||||
// http-on-modify-request. Keep it so that we can associate it with the
|
||||
// channel there - will be needed in case of redirect.
|
||||
PolicyRemote.previousRequest = [Utils.unwrapURL(contentLocation), contentType];
|
||||
}
|
||||
else if (result.postProcess)
|
||||
Utils.schedulePostProcess(node);
|
||||
return result.value;
|
||||
},
|
||||
|
||||
shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra)
|
||||
{
|
||||
return Ci.nsIContentPolicy.ACCEPT;
|
||||
},
|
||||
|
||||
//
|
||||
// nsIObserver interface implementation
|
||||
//
|
||||
observe: function(subject, topic, data, additional)
|
||||
{
|
||||
switch (topic)
|
||||
{
|
||||
case "content-document-global-created":
|
||||
{
|
||||
if (!(subject instanceof Ci.nsIDOMWindow) || !subject.opener)
|
||||
return;
|
||||
|
||||
let uri = additional || Utils.makeURI(subject.location.href);
|
||||
if (PolicyRemote.shouldLoad(0xFFFE /*Policy.type.POPUP*/, uri, null, subject.opener.document, null, null) != Ci.nsIContentPolicy.ACCEPT)
|
||||
{
|
||||
subject.stop();
|
||||
Utils.runAsync(subject.close, subject);
|
||||
}
|
||||
else if (uri.spec == "about:blank")
|
||||
{
|
||||
// An about:blank pop-up most likely means that a load will be
|
||||
// initiated synchronously. Set a flag for our "http-on-modify-request"
|
||||
// handler.
|
||||
PolicyRemote.expectingPopupLoad = true;
|
||||
Utils.runAsync(function()
|
||||
{
|
||||
PolicyRemote.expectingPopupLoad = false;
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "http-on-modify-request":
|
||||
{
|
||||
if (!(subject instanceof Ci.nsIHttpChannel))
|
||||
return;
|
||||
|
||||
// TODO: Do-not-track header
|
||||
|
||||
if (PolicyRemote.previousRequest && subject.URI == PolicyRemote.previousRequest[0] &&
|
||||
subject instanceof Ci.nsIWritablePropertyBag)
|
||||
{
|
||||
// We just handled a content policy call for this request - associate
|
||||
// the data with the channel so that we can find it in case of a redirect.
|
||||
subject.setProperty("abpRequestType", PolicyRemote.previousRequest[1]);
|
||||
PolicyRemote.previousRequest = null;
|
||||
}
|
||||
|
||||
if (PolicyRemote.expectingPopupLoad)
|
||||
{
|
||||
let wnd = Utils.getRequestWindow(subject);
|
||||
if (wnd && wnd.opener && wnd.location.href == "about:blank")
|
||||
PolicyRemote.observe(wnd, "content-document-global-created", null, subject.URI);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// nsIChannelEventSink interface implementation
|
||||
//
|
||||
|
||||
onChannelRedirect: function(oldChannel, newChannel, flags)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to retrieve previously stored request data from the channel
|
||||
let contentType;
|
||||
if (oldChannel instanceof Ci.nsIWritablePropertyBag)
|
||||
{
|
||||
try
|
||||
{
|
||||
contentType = oldChannel.getProperty("abpRequestType");
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
// No data attached, ignore this redirect
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let newLocation = null;
|
||||
try
|
||||
{
|
||||
newLocation = newChannel.URI;
|
||||
} catch(e2) {}
|
||||
if (!newLocation)
|
||||
return;
|
||||
|
||||
let wnd = Utils.getRequestWindow(newChannel);
|
||||
if (!wnd)
|
||||
return;
|
||||
|
||||
// HACK: NS_BINDING_ABORTED would be proper error code to throw but this will show up in error console (bug 287107)
|
||||
if (PolicyRemote.shouldLoad(contentType, newLocation, null, wnd.document) != Ci.nsIContentPolicy.ACCEPT)
|
||||
throw Cr.NS_BASE_STREAM_WOULD_BLOCK;
|
||||
else
|
||||
return;
|
||||
}
|
||||
catch (e if (e != Cr.NS_BASE_STREAM_WOULD_BLOCK))
|
||||
{
|
||||
// We shouldn't throw exceptions here - this will prevent the redirect.
|
||||
Cu.reportError(e);
|
||||
}
|
||||
},
|
||||
|
||||
asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback)
|
||||
{
|
||||
this.onChannelRedirect(oldChannel, newChannel, flags);
|
||||
|
||||
// If onChannelRedirect didn't throw an exception indicate success
|
||||
callback.onRedirectVerifyCallback(Cr.NS_OK);
|
||||
},
|
||||
|
||||
//
|
||||
// nsIFactory interface implementation
|
||||
//
|
||||
|
||||
createInstance: function(outer, iid)
|
||||
{
|
||||
if (outer)
|
||||
throw Cr.NS_ERROR_NO_AGGREGATION;
|
||||
return this.QueryInterface(iid);
|
||||
}
|
||||
};
|
||||
|
||||
PolicyRemote.startup();
|
||||
@@ -0,0 +1,447 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Element hiding implementation.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["ElemHide"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
Cu.import(baseURL + "IO.jsm");
|
||||
Cu.import(baseURL + "Prefs.jsm");
|
||||
Cu.import(baseURL + "ContentPolicy.jsm");
|
||||
Cu.import(baseURL + "FilterNotifier.jsm");
|
||||
Cu.import(baseURL + "FilterClasses.jsm");
|
||||
Cu.import(baseURL + "TimeLine.jsm");
|
||||
|
||||
/**
|
||||
* Lookup table, filters by their associated key
|
||||
* @type Object
|
||||
*/
|
||||
let filterByKey = {__proto__: null};
|
||||
|
||||
/**
|
||||
* Lookup table, keys of the filters by filter text
|
||||
* @type Object
|
||||
*/
|
||||
let keyByFilter = {__proto__: null};
|
||||
|
||||
/**
|
||||
* Currently applied stylesheet URL
|
||||
* @type nsIURI
|
||||
*/
|
||||
let styleURL = null;
|
||||
|
||||
/**
|
||||
* Element hiding component
|
||||
* @class
|
||||
*/
|
||||
var ElemHide =
|
||||
{
|
||||
/**
|
||||
* Indicates whether filters have been added or removed since the last apply() call.
|
||||
* @type Boolean
|
||||
*/
|
||||
isDirty: false,
|
||||
|
||||
/**
|
||||
* Inidicates whether the element hiding stylesheet is currently applied.
|
||||
* @type Boolean
|
||||
*/
|
||||
applied: false,
|
||||
|
||||
/**
|
||||
* Called on module startup.
|
||||
*/
|
||||
init: function()
|
||||
{
|
||||
TimeLine.enter("Entered ElemHide.init()");
|
||||
Prefs.addListener(function(name)
|
||||
{
|
||||
if (name == "enabled")
|
||||
ElemHide.apply();
|
||||
});
|
||||
|
||||
TimeLine.log("done adding prefs listener");
|
||||
|
||||
let styleFile = IO.resolveFilePath(Prefs.data_directory);
|
||||
styleFile.append("elemhide.css");
|
||||
styleURL = Utils.ioService.newFileURI(styleFile).QueryInterface(Ci.nsIFileURL);
|
||||
TimeLine.log("done determining stylesheet URL");
|
||||
|
||||
TimeLine.log("registering component");
|
||||
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
registrar.registerFactory(ElemHidePrivate.classID, ElemHidePrivate.classDescription,
|
||||
"@mozilla.org/network/protocol/about;1?what=" + ElemHidePrivate.aboutPrefix, ElemHidePrivate);
|
||||
|
||||
TimeLine.leave("ElemHide.init() done");
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes all known filters
|
||||
*/
|
||||
clear: function()
|
||||
{
|
||||
filterByKey = {__proto__: null};
|
||||
keyByFilter = {__proto__: null};
|
||||
ElemHide.isDirty = false;
|
||||
ElemHide.unapply();
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new element hiding filter
|
||||
* @param {ElemHideFilter} filter
|
||||
*/
|
||||
add: function(filter)
|
||||
{
|
||||
if (filter.text in keyByFilter)
|
||||
return;
|
||||
|
||||
let key;
|
||||
do {
|
||||
key = Math.random().toFixed(15).substr(5);
|
||||
} while (key in filterByKey);
|
||||
|
||||
filterByKey[key] = filter;
|
||||
keyByFilter[filter.text] = key;
|
||||
ElemHide.isDirty = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes an element hiding filter
|
||||
* @param {ElemHideFilter} filter
|
||||
*/
|
||||
remove: function(filter)
|
||||
{
|
||||
if (!(filter.text in keyByFilter))
|
||||
return;
|
||||
|
||||
let key = keyByFilter[filter.text];
|
||||
delete filterByKey[key];
|
||||
delete keyByFilter[filter.text];
|
||||
ElemHide.isDirty = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Will be set to true if apply() is running (reentrance protection).
|
||||
* @type Boolean
|
||||
*/
|
||||
_applying: false,
|
||||
|
||||
/**
|
||||
* Will be set to true if an apply() call arrives while apply() is already
|
||||
* running (delayed execution).
|
||||
* @type Boolean
|
||||
*/
|
||||
_needsApply: false,
|
||||
|
||||
/**
|
||||
* Generates stylesheet URL and applies it globally
|
||||
*/
|
||||
apply: function()
|
||||
{
|
||||
if (this._applying)
|
||||
{
|
||||
this._needsApply = true;
|
||||
return;
|
||||
}
|
||||
|
||||
TimeLine.enter("Entered ElemHide.apply()");
|
||||
|
||||
if (!ElemHide.isDirty || !Prefs.enabled)
|
||||
{
|
||||
// Nothing changed, looks like we merely got enabled/disabled
|
||||
if (Prefs.enabled && !ElemHide.applied)
|
||||
{
|
||||
try
|
||||
{
|
||||
Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
|
||||
ElemHide.applied = true;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
}
|
||||
TimeLine.log("Applying existing stylesheet finished");
|
||||
}
|
||||
else if (!Prefs.enabled && ElemHide.applied)
|
||||
{
|
||||
ElemHide.unapply();
|
||||
TimeLine.log("ElemHide.unapply() finished");
|
||||
}
|
||||
|
||||
TimeLine.leave("ElemHide.apply() done (no file changes)");
|
||||
return;
|
||||
}
|
||||
|
||||
IO.writeToFile(styleURL.file, false, this._generateCSSContent(), function(e)
|
||||
{
|
||||
TimeLine.enter("ElemHide.apply() write callback");
|
||||
this._applying = false;
|
||||
|
||||
if (e && e.result == Cr.NS_ERROR_NOT_AVAILABLE)
|
||||
{
|
||||
e = null;
|
||||
try
|
||||
{
|
||||
styleURL.file.remove(false);
|
||||
} catch (e2) {}
|
||||
}
|
||||
else if (e)
|
||||
Cu.reportError(e);
|
||||
|
||||
if (this._needsApply)
|
||||
{
|
||||
this._needsApply = false;
|
||||
this.apply();
|
||||
}
|
||||
else if (!e)
|
||||
{
|
||||
ElemHide.isDirty = false;
|
||||
|
||||
ElemHide.unapply();
|
||||
TimeLine.log("ElemHide.unapply() finished");
|
||||
|
||||
if (styleURL.file.exists())
|
||||
{
|
||||
try
|
||||
{
|
||||
Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
|
||||
ElemHide.applied = true;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
}
|
||||
TimeLine.log("Applying stylesheet finished");
|
||||
}
|
||||
|
||||
FilterNotifier.triggerListeners("elemhideupdate");
|
||||
}
|
||||
TimeLine.leave("ElemHide.apply() write callback done");
|
||||
}.bind(this), "ElemHideWrite");
|
||||
|
||||
this._applying = true;
|
||||
|
||||
TimeLine.leave("ElemHide.apply() done", "ElemHideWrite");
|
||||
},
|
||||
|
||||
_generateCSSContent: function()
|
||||
{
|
||||
// Grouping selectors by domains
|
||||
TimeLine.log("start grouping selectors");
|
||||
let domains = {__proto__: null};
|
||||
let hasFilters = false;
|
||||
for (let key in filterByKey)
|
||||
{
|
||||
let filter = filterByKey[key];
|
||||
let domain = filter.selectorDomain || "";
|
||||
|
||||
let list;
|
||||
if (domain in domains)
|
||||
list = domains[domain];
|
||||
else
|
||||
{
|
||||
list = {__proto__: null};
|
||||
domains[domain] = list;
|
||||
}
|
||||
list[filter.selector] = key;
|
||||
hasFilters = true;
|
||||
}
|
||||
TimeLine.log("done grouping selectors");
|
||||
|
||||
if (!hasFilters)
|
||||
throw Cr.NS_ERROR_NOT_AVAILABLE;
|
||||
|
||||
function escapeChar(match)
|
||||
{
|
||||
return "\\" + match.charCodeAt(0).toString(16) + " ";
|
||||
}
|
||||
|
||||
// Return CSS data
|
||||
let cssTemplate = "-moz-binding: url(about:" + ElemHidePrivate.aboutPrefix + "?%ID%#dummy) !important;";
|
||||
for (let domain in domains)
|
||||
{
|
||||
let rules = [];
|
||||
let list = domains[domain];
|
||||
|
||||
if (domain)
|
||||
yield ('@-moz-document domain("' + domain.split(",").join('"),domain("') + '"){').replace(/[^\x01-\x7F]/g, escapeChar);
|
||||
else
|
||||
{
|
||||
// Only allow unqualified rules on a few protocols to prevent them from blocking chrome
|
||||
yield '@-moz-document url-prefix("http://"),url-prefix("https://"),'
|
||||
+ 'url-prefix("mailbox://"),url-prefix("imap://"),'
|
||||
+ 'url-prefix("news://"),url-prefix("snews://"){';
|
||||
}
|
||||
|
||||
for (let selector in list)
|
||||
yield selector.replace(/[^\x01-\x7F]/g, escapeChar) + "{" + cssTemplate.replace("%ID%", list[selector]) + "}";
|
||||
yield '}';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Unapplies current stylesheet URL
|
||||
*/
|
||||
unapply: function()
|
||||
{
|
||||
if (ElemHide.applied)
|
||||
{
|
||||
try
|
||||
{
|
||||
Utils.styleService.unregisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
}
|
||||
ElemHide.applied = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the currently applied stylesheet URL
|
||||
* @type String
|
||||
*/
|
||||
get styleURL() ElemHide.applied ? styleURL.spec : null,
|
||||
|
||||
/**
|
||||
* Retrieves an element hiding filter by the corresponding protocol key
|
||||
*/
|
||||
getFilterByKey: function(/**String*/ key) /**Filter*/
|
||||
{
|
||||
return (key in filterByKey ? filterByKey[key] : null);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Private nsIAboutModule implementation
|
||||
* @class
|
||||
*/
|
||||
var ElemHidePrivate =
|
||||
{
|
||||
classID: Components.ID("{55fb7be0-1dd2-11b2-98e6-9e97caf8ba67}"),
|
||||
classDescription: "Element hiding hit registration protocol handler",
|
||||
aboutPrefix: "abp-elemhidehit",
|
||||
|
||||
//
|
||||
// Factory implementation
|
||||
//
|
||||
|
||||
createInstance: function(outer, iid)
|
||||
{
|
||||
if (outer != null)
|
||||
throw Cr.NS_ERROR_NO_AGGREGATION;
|
||||
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
|
||||
//
|
||||
// About module implementation
|
||||
//
|
||||
|
||||
getURIFlags: function(uri)
|
||||
{
|
||||
return ("HIDE_FROM_ABOUTABOUT" in Ci.nsIAboutModule ? Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT : 0);
|
||||
},
|
||||
|
||||
newChannel: function(uri)
|
||||
{
|
||||
let match = /\?(\d+)/.exec(uri.path)
|
||||
if (!match)
|
||||
throw Cr.NS_ERROR_FAILURE;
|
||||
|
||||
return new HitRegistrationChannel(uri, match[1]);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory, Ci.nsIAboutModule])
|
||||
};
|
||||
|
||||
/**
|
||||
* Channel returning data for element hiding hits.
|
||||
* @constructor
|
||||
*/
|
||||
function HitRegistrationChannel(uri, key)
|
||||
{
|
||||
this.key = key;
|
||||
this.URI = this.originalURI = uri;
|
||||
}
|
||||
HitRegistrationChannel.prototype = {
|
||||
key: null,
|
||||
URI: null,
|
||||
originalURI: null,
|
||||
contentCharset: "utf-8",
|
||||
contentLength: 0,
|
||||
contentType: "text/xml",
|
||||
owner: Utils.systemPrincipal,
|
||||
securityInfo: null,
|
||||
notificationCallbacks: null,
|
||||
loadFlags: 0,
|
||||
loadGroup: null,
|
||||
name: null,
|
||||
status: Cr.NS_OK,
|
||||
|
||||
asyncOpen: function(listener, context)
|
||||
{
|
||||
let stream = this.open();
|
||||
Utils.runAsync(function()
|
||||
{
|
||||
try {
|
||||
listener.onStartRequest(this, context);
|
||||
} catch(e) {}
|
||||
try {
|
||||
listener.onDataAvailable(this, context, stream, 0, stream.available());
|
||||
} catch(e) {}
|
||||
try {
|
||||
listener.onStopRequest(this, context, Cr.NS_OK);
|
||||
} catch(e) {}
|
||||
}, this);
|
||||
},
|
||||
|
||||
open: function()
|
||||
{
|
||||
let data = "<bindings xmlns='http://www.mozilla.org/xbl'><binding id='dummy' bindToUntrustedContent='true'/></bindings>";
|
||||
if (this.key in filterByKey)
|
||||
{
|
||||
let wnd = Utils.getRequestWindow(this);
|
||||
if (wnd && wnd.document && !Policy.processNode(wnd, wnd.document, Policy.type.ELEMHIDE, filterByKey[this.key]))
|
||||
data = "<bindings xmlns='http://www.mozilla.org/xbl'/>";
|
||||
}
|
||||
|
||||
let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
||||
stream.setData(data, data.length);
|
||||
return stream;
|
||||
},
|
||||
isPending: function()
|
||||
{
|
||||
return false;
|
||||
},
|
||||
cancel: function()
|
||||
{
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
suspend: function()
|
||||
{
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
resume: function()
|
||||
{
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
|
||||
};
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Element hiding protocol to be loaded in the content process for a multi-process setup (currently only Fennec)
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["ElemHideRemote"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://@ADDON_CHROME_NAME@/modules/Utils.jsm");
|
||||
|
||||
/**
|
||||
* Currently applied stylesheet URL
|
||||
* @type nsIURI
|
||||
*/
|
||||
let styleURL = null;
|
||||
|
||||
/**
|
||||
* nsIAboutModule implementation
|
||||
* @class
|
||||
*/
|
||||
var ElemHideRemote =
|
||||
{
|
||||
classID: Components.ID("{55fb7be0-1dd2-11b2-98e6-9e97caf8ba67}"),
|
||||
classDescription: "Element hiding hit registration protocol handler",
|
||||
aboutPrefix: "abp-elemhidehit",
|
||||
|
||||
startup: function()
|
||||
{
|
||||
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
registrar.registerFactory(ElemHideRemote.classID, ElemHideRemote.classDescription,
|
||||
"@mozilla.org/network/protocol/about;1?what=" + ElemHideRemote.aboutPrefix, ElemHideRemote);
|
||||
|
||||
styleURL = Utils.makeURI(Utils.childMessageManager.sendSyncMessage("AdblockPlus:ElemHide:styleURL"));
|
||||
if (styleURL)
|
||||
Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
|
||||
|
||||
// Get notified about style URL changes
|
||||
Utils.childMessageManager.addMessageListener("AdblockPlus:ElemHide:updateStyleURL", function(message)
|
||||
{
|
||||
if (styleURL)
|
||||
Utils.styleService.unregisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
|
||||
|
||||
styleURL = Utils.makeURI(message.json);
|
||||
if (styleURL)
|
||||
Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
|
||||
});
|
||||
},
|
||||
|
||||
//
|
||||
// Factory implementation
|
||||
//
|
||||
|
||||
createInstance: function(outer, iid)
|
||||
{
|
||||
if (outer != null)
|
||||
throw Cr.NS_ERROR_NO_AGGREGATION;
|
||||
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
|
||||
//
|
||||
// About module implementation
|
||||
//
|
||||
|
||||
getURIFlags: function(uri)
|
||||
{
|
||||
return Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
|
||||
},
|
||||
|
||||
newChannel: function(uri)
|
||||
{
|
||||
let match = /\?(\d+)/.exec(uri.path)
|
||||
if (!match)
|
||||
throw Cr.NS_ERROR_FAILURE;
|
||||
|
||||
return new HitRegistrationChannel(uri, match[1]);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory, Ci.nsIAboutModule])
|
||||
};
|
||||
|
||||
/**
|
||||
* Channel returning data for element hiding hits.
|
||||
* @constructor
|
||||
*/
|
||||
function HitRegistrationChannel(uri, key)
|
||||
{
|
||||
this.key = key;
|
||||
this.URI = this.originalURI = uri;
|
||||
}
|
||||
HitRegistrationChannel.prototype = {
|
||||
key: null,
|
||||
URI: null,
|
||||
originalURI: null,
|
||||
contentCharset: "utf-8",
|
||||
contentLength: 0,
|
||||
contentType: "text/xml",
|
||||
owner: Utils.systemPrincipal,
|
||||
securityInfo: null,
|
||||
notificationCallbacks: null,
|
||||
loadFlags: 0,
|
||||
loadGroup: null,
|
||||
name: null,
|
||||
status: Cr.NS_OK,
|
||||
|
||||
asyncOpen: function(listener, context)
|
||||
{
|
||||
let stream = this.open();
|
||||
Utils.runAsync(function()
|
||||
{
|
||||
try {
|
||||
listener.onStartRequest(this, context);
|
||||
} catch(e) {}
|
||||
try {
|
||||
listener.onDataAvailable(this, context, stream, 0, stream.available());
|
||||
} catch(e) {}
|
||||
try {
|
||||
listener.onStopRequest(this, context, Cr.NS_OK);
|
||||
} catch(e) {}
|
||||
}, this);
|
||||
},
|
||||
|
||||
open: function()
|
||||
{
|
||||
let data = "<bindings xmlns='http://www.mozilla.org/xbl'><binding id='dummy' bindToUntrustedContent='true'/></bindings>";
|
||||
let wnd = Utils.getRequestWindow(this);
|
||||
|
||||
if (wnd)
|
||||
{
|
||||
wnd = Utils.getOriginWindow(wnd);
|
||||
|
||||
let locations = [];
|
||||
let testWnd = wnd;
|
||||
while (true)
|
||||
{
|
||||
locations.push(testWnd.location.href);
|
||||
if (testWnd.parent == testWnd)
|
||||
break;
|
||||
else
|
||||
testWnd = testWnd.parent;
|
||||
}
|
||||
|
||||
let result = Utils.childMessageManager.sendSyncMessage("AdblockPlus:ElemHide:checkHit", {
|
||||
key: this.key,
|
||||
locations: locations})[0];
|
||||
if (result)
|
||||
data = "<bindings xmlns='http://www.mozilla.org/xbl'/>";
|
||||
}
|
||||
|
||||
let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
||||
stream.setData(data, data.length);
|
||||
return stream;
|
||||
},
|
||||
isPending: function()
|
||||
{
|
||||
return false;
|
||||
},
|
||||
cancel: function()
|
||||
{
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
suspend: function()
|
||||
{
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
resume: function()
|
||||
{
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
|
||||
};
|
||||
|
||||
ElemHideRemote.startup();
|
||||
@@ -0,0 +1,813 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Definition of Filter class and its subclasses.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Filter", "InvalidFilter", "CommentFilter", "ActiveFilter", "RegExpFilter", "BlockingFilter", "WhitelistFilter", "ElemHideFilter"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
Cu.import(baseURL + "FilterNotifier.jsm");
|
||||
|
||||
/**
|
||||
* Abstract base class for filters
|
||||
*
|
||||
* @param {String} text string representation of the filter
|
||||
* @constructor
|
||||
*/
|
||||
function Filter(text)
|
||||
{
|
||||
this.text = text;
|
||||
this.subscriptions = [];
|
||||
}
|
||||
Filter.prototype =
|
||||
{
|
||||
/**
|
||||
* String representation of the filter
|
||||
* @type String
|
||||
*/
|
||||
text: null,
|
||||
|
||||
/**
|
||||
* Filter subscriptions the filter belongs to
|
||||
* @type Array of Subscription
|
||||
*/
|
||||
subscriptions: null,
|
||||
|
||||
/**
|
||||
* Serializes the filter to an array of strings for writing out on the disk.
|
||||
* @param {Array of String} buffer buffer to push the serialization results into
|
||||
*/
|
||||
serialize: function(buffer)
|
||||
{
|
||||
buffer.push("[Filter]");
|
||||
buffer.push("text=" + this.text);
|
||||
},
|
||||
|
||||
toString: function()
|
||||
{
|
||||
return this.text;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache for known filters, maps string representation to filter objects.
|
||||
* @type Object
|
||||
*/
|
||||
Filter.knownFilters = {__proto__: null};
|
||||
|
||||
/**
|
||||
* Regular expression that element hiding filters should match
|
||||
* @type RegExp
|
||||
*/
|
||||
Filter.elemhideRegExp = /^([^\/\*\|\@"!]*?)#(?:([\w\-]+|\*)((?:\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\))*)|#([^{}]+))$/;
|
||||
/**
|
||||
* Regular expression that RegExp filters specified as RegExps should match
|
||||
* @type RegExp
|
||||
*/
|
||||
Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)?$/;
|
||||
/**
|
||||
* Regular expression that options on a RegExp filter should match
|
||||
* @type RegExp
|
||||
*/
|
||||
Filter.optionsRegExp = /\$(~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)$/;
|
||||
|
||||
/**
|
||||
* Creates a filter of correct type from its text representation - does the basic parsing and
|
||||
* calls the right constructor then.
|
||||
*
|
||||
* @param {String} text as in Filter()
|
||||
* @return {Filter} filter or null if the filter couldn't be created
|
||||
*/
|
||||
Filter.fromText = function(text)
|
||||
{
|
||||
if (text in Filter.knownFilters)
|
||||
return Filter.knownFilters[text];
|
||||
|
||||
if (!/\S/.test(text))
|
||||
return null;
|
||||
|
||||
let ret;
|
||||
let match = Filter.elemhideRegExp.exec(text);
|
||||
if (match)
|
||||
ret = ElemHideFilter.fromText(text, match[1], match[2], match[3], match[4]);
|
||||
else if (text[0] == "!")
|
||||
ret = new CommentFilter(text);
|
||||
else
|
||||
ret = RegExpFilter.fromText(text);
|
||||
|
||||
Filter.knownFilters[ret.text] = ret;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a filter
|
||||
*
|
||||
* @param {Object} obj map of serialized properties and their values
|
||||
* @return {Filter} filter or null if the filter couldn't be created
|
||||
*/
|
||||
Filter.fromObject = function(obj)
|
||||
{
|
||||
let ret = Filter.fromText(obj.text);
|
||||
if (ret instanceof ActiveFilter)
|
||||
{
|
||||
if ("disabled" in obj)
|
||||
ret._disabled = (obj.disabled == "true");
|
||||
if ("hitCount" in obj)
|
||||
ret._hitCount = parseInt(obj.hitCount) || 0;
|
||||
if ("lastHit" in obj)
|
||||
ret._lastHit = parseInt(obj.lastHit) || 0;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes unnecessary whitespaces from filter text, will only return null if
|
||||
* the input parameter is null.
|
||||
*/
|
||||
Filter.normalize = function(/**String*/ text) /**String*/
|
||||
{
|
||||
if (!text)
|
||||
return text;
|
||||
|
||||
// Remove line breaks and such
|
||||
text = text.replace(/[^\S ]/g, "");
|
||||
|
||||
if (/^\s*!/.test(text))
|
||||
{
|
||||
// Don't remove spaces inside comments
|
||||
return text.replace(/^\s+/, "").replace(/\s+$/, "");
|
||||
}
|
||||
else if (Filter.elemhideRegExp.test(text))
|
||||
{
|
||||
// Special treatment for element hiding filters, right side is allowed to contain spaces
|
||||
let [, domain, separator, selector] = /^(.*?)(#+)(.*)$/.exec(text); // .split(..., 2) will cut off the end of the string
|
||||
return domain.replace(/\s/g, "") + separator + selector.replace(/^\s+/, "").replace(/\s+$/, "");
|
||||
}
|
||||
else
|
||||
return text.replace(/\s/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for invalid filters
|
||||
* @param {String} text see Filter()
|
||||
* @param {String} reason Reason why this filter is invalid
|
||||
* @constructor
|
||||
* @augments Filter
|
||||
*/
|
||||
function InvalidFilter(text, reason)
|
||||
{
|
||||
Filter.call(this, text);
|
||||
|
||||
this.reason = reason;
|
||||
}
|
||||
InvalidFilter.prototype =
|
||||
{
|
||||
__proto__: Filter.prototype,
|
||||
|
||||
/**
|
||||
* Reason why this filter is invalid
|
||||
* @type String
|
||||
*/
|
||||
reason: null,
|
||||
|
||||
/**
|
||||
* See Filter.serialize()
|
||||
*/
|
||||
serialize: function(buffer) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Class for comments
|
||||
* @param {String} text see Filter()
|
||||
* @constructor
|
||||
* @augments Filter
|
||||
*/
|
||||
function CommentFilter(text)
|
||||
{
|
||||
Filter.call(this, text);
|
||||
}
|
||||
CommentFilter.prototype =
|
||||
{
|
||||
__proto__: Filter.prototype,
|
||||
|
||||
/**
|
||||
* See Filter.serialize()
|
||||
*/
|
||||
serialize: function(buffer) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstract base class for filters that can get hits
|
||||
* @param {String} text see Filter()
|
||||
* @param {String} domains (optional) Domains that the filter is restricted to separated by domainSeparator e.g. "foo.com|bar.com|~baz.com"
|
||||
* @constructor
|
||||
* @augments Filter
|
||||
*/
|
||||
function ActiveFilter(text, domains)
|
||||
{
|
||||
Filter.call(this, text);
|
||||
|
||||
if (domains)
|
||||
{
|
||||
this.domainSource = domains;
|
||||
this.__defineGetter__("domains", this._getDomains);
|
||||
}
|
||||
}
|
||||
ActiveFilter.prototype =
|
||||
{
|
||||
__proto__: Filter.prototype,
|
||||
|
||||
_disabled: false,
|
||||
_hitCount: 0,
|
||||
_lastHit: 0,
|
||||
|
||||
/**
|
||||
* Defines whether the filter is disabled
|
||||
* @type Boolean
|
||||
*/
|
||||
get disabled() this._disabled,
|
||||
set disabled(value)
|
||||
{
|
||||
if (value != this._disabled)
|
||||
{
|
||||
let oldValue = this._disabled;
|
||||
this._disabled = value;
|
||||
FilterNotifier.triggerListeners("filter.disabled", this, value, oldValue);
|
||||
}
|
||||
return this._disabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Number of hits on the filter since the last reset
|
||||
* @type Number
|
||||
*/
|
||||
get hitCount() this._hitCount,
|
||||
set hitCount(value)
|
||||
{
|
||||
if (value != this._hitCount)
|
||||
{
|
||||
let oldValue = this._hitCount;
|
||||
this._hitCount = value;
|
||||
FilterNotifier.triggerListeners("filter.hitCount", this, value, oldValue);
|
||||
}
|
||||
return this._hitCount;
|
||||
},
|
||||
|
||||
/**
|
||||
* Last time the filter had a hit (in milliseconds since the beginning of the epoch)
|
||||
* @type Number
|
||||
*/
|
||||
get lastHit() this._lastHit,
|
||||
set lastHit(value)
|
||||
{
|
||||
if (value != this._lastHit)
|
||||
{
|
||||
let oldValue = this._lastHit;
|
||||
this._lastHit = value;
|
||||
FilterNotifier.triggerListeners("filter.lastHit", this, value, oldValue);
|
||||
}
|
||||
return this._lastHit;
|
||||
},
|
||||
|
||||
/**
|
||||
* String that the domains property should be generated from
|
||||
* @type String
|
||||
*/
|
||||
domainSource: null,
|
||||
|
||||
/**
|
||||
* Separator character used in domainSource property, must be overridden by subclasses
|
||||
* @type String
|
||||
*/
|
||||
domainSeparator: null,
|
||||
|
||||
/**
|
||||
* Map containing domains that this filter should match on/not match on or null if the filter should match on all domains
|
||||
* @type Object
|
||||
*/
|
||||
domains: null,
|
||||
|
||||
/**
|
||||
* Called first time domains property is requested, triggers _generateDomains method.
|
||||
*/
|
||||
_getDomains: function()
|
||||
{
|
||||
this._generateDomains();
|
||||
return this.domains;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates domains property when it is requested for the first time.
|
||||
*/
|
||||
_generateDomains: function()
|
||||
{
|
||||
let domains = this.domainSource.split(this.domainSeparator);
|
||||
|
||||
delete this.domainSource;
|
||||
delete this.domains;
|
||||
|
||||
if (domains.length == 1 && domains[0][0] != "~")
|
||||
{
|
||||
// Fast track for the common one-domain scenario
|
||||
this.domains = {__proto__: null, "": false};
|
||||
this.domains[domains[0]] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
let hasIncludes = false;
|
||||
for (let i = 0; i < domains.length; i++)
|
||||
{
|
||||
let domain = domains[i];
|
||||
if (domain == "")
|
||||
continue;
|
||||
|
||||
let include;
|
||||
if (domain[0] == "~")
|
||||
{
|
||||
include = false;
|
||||
domain = domain.substr(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
include = true;
|
||||
hasIncludes = true;
|
||||
}
|
||||
|
||||
if (!this.domains)
|
||||
this.domains = {__proto__: null};
|
||||
|
||||
this.domains[domain] = include;
|
||||
}
|
||||
this.domains[""] = !hasIncludes;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether this filter is active on a domain.
|
||||
*/
|
||||
isActiveOnDomain: function(/**String*/ docDomain) /**Boolean*/
|
||||
{
|
||||
// If no domains are set the rule matches everywhere
|
||||
if (!this.domains)
|
||||
return true;
|
||||
|
||||
// If the document has no host name, match only if the filter isn't restricted to specific domains
|
||||
if (!docDomain)
|
||||
return this.domains[""];
|
||||
|
||||
docDomain = docDomain.replace(/\.+$/, "").toUpperCase();
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (docDomain in this.domains)
|
||||
return this.domains[docDomain];
|
||||
|
||||
let nextDot = docDomain.indexOf(".");
|
||||
if (nextDot < 0)
|
||||
break;
|
||||
docDomain = docDomain.substr(nextDot + 1);
|
||||
}
|
||||
return this.domains[""];
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether this filter is active only on a domain and its subdomains.
|
||||
*/
|
||||
isActiveOnlyOnDomain: function(/**String*/ docDomain) /**Boolean*/
|
||||
{
|
||||
if (!docDomain || !this.domains || this.domains[""])
|
||||
return false;
|
||||
|
||||
docDomain = docDomain.replace(/\.+$/, "").toUpperCase();
|
||||
|
||||
for (let domain in this.domains)
|
||||
if (this.domains[domain] && domain != docDomain && (domain.length <= docDomain.length || domain.indexOf("." + docDomain) != domain.length - docDomain.length - 1))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* See Filter.serialize()
|
||||
*/
|
||||
serialize: function(buffer)
|
||||
{
|
||||
if (this._disabled || this._hitCount || this._lastHit)
|
||||
{
|
||||
Filter.prototype.serialize.call(this, buffer);
|
||||
if (this._disabled)
|
||||
buffer.push("disabled=true");
|
||||
if (this._hitCount)
|
||||
buffer.push("hitCount=" + this._hitCount);
|
||||
if (this._lastHit)
|
||||
buffer.push("lastHit=" + this._lastHit);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstract base class for RegExp-based filters
|
||||
* @param {String} text see Filter()
|
||||
* @param {String} regexpSource filter part that the regular expression should be build from
|
||||
* @param {Number} contentType (optional) Content types the filter applies to, combination of values from RegExpFilter.typeMap
|
||||
* @param {Boolean} matchCase (optional) Defines whether the filter should distinguish between lower and upper case letters
|
||||
* @param {String} domains (optional) Domains that the filter is restricted to, e.g. "foo.com|bar.com|~baz.com"
|
||||
* @param {Boolean} thirdParty (optional) Defines whether the filter should apply to third-party or first-party content only
|
||||
* @constructor
|
||||
* @augments ActiveFilter
|
||||
*/
|
||||
function RegExpFilter(text, regexpSource, contentType, matchCase, domains, thirdParty)
|
||||
{
|
||||
ActiveFilter.call(this, text, domains);
|
||||
|
||||
if (contentType != null)
|
||||
this.contentType = contentType;
|
||||
if (matchCase)
|
||||
this.matchCase = matchCase;
|
||||
if (thirdParty != null)
|
||||
this.thirdParty = thirdParty;
|
||||
|
||||
if (regexpSource.length >= 2 && regexpSource[0] == "/" && regexpSource[regexpSource.length - 1] == "/")
|
||||
{
|
||||
// The filter is a regular expression - convert it immediately to catch syntax errors
|
||||
this.regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), this.matchCase ? "" : "i");
|
||||
}
|
||||
else
|
||||
{
|
||||
// No need to convert this filter to regular expression yet, do it on demand
|
||||
this.regexpSource = regexpSource;
|
||||
this.__defineGetter__("regexp", this._generateRegExp);
|
||||
}
|
||||
}
|
||||
RegExpFilter.prototype =
|
||||
{
|
||||
__proto__: ActiveFilter.prototype,
|
||||
|
||||
/**
|
||||
* Number of filters contained, will always be 1 (required to optimize Matcher).
|
||||
* @type Integer
|
||||
*/
|
||||
length: 1,
|
||||
|
||||
/**
|
||||
* @see ActiveFilter.domainSeparator
|
||||
*/
|
||||
domainSeparator: "|",
|
||||
|
||||
/**
|
||||
* Expression from which a regular expression should be generated - for delayed creation of the regexp property
|
||||
* @type String
|
||||
*/
|
||||
regexpSource: null,
|
||||
/**
|
||||
* Regular expression to be used when testing against this filter
|
||||
* @type RegExp
|
||||
*/
|
||||
regexp: null,
|
||||
/**
|
||||
* Content types the filter applies to, combination of values from RegExpFilter.typeMap
|
||||
* @type Number
|
||||
*/
|
||||
contentType: 0x7FFFFFFF,
|
||||
/**
|
||||
* Defines whether the filter should distinguish between lower and upper case letters
|
||||
* @type Boolean
|
||||
*/
|
||||
matchCase: false,
|
||||
/**
|
||||
* Defines whether the filter should apply to third-party or first-party content only. Can be null (apply to all content).
|
||||
* @type Boolean
|
||||
*/
|
||||
thirdParty: null,
|
||||
|
||||
/**
|
||||
* Generates regexp property when it is requested for the first time.
|
||||
* @return {RegExp}
|
||||
*/
|
||||
_generateRegExp: function()
|
||||
{
|
||||
// Remove multiple wildcards
|
||||
let source = this.regexpSource.replace(/\*+/g, "*");
|
||||
|
||||
// Remove leading wildcards
|
||||
if (source[0] == "*")
|
||||
source = source.substr(1);
|
||||
|
||||
// Remove trailing wildcards
|
||||
let pos = source.length - 1;
|
||||
if (pos >= 0 && source[pos] == "*")
|
||||
source = source.substr(0, pos);
|
||||
|
||||
source = source.replace(/\^\|$/, "^") // remove anchors following separator placeholder
|
||||
.replace(/\W/g, "\\$&") // escape special symbols
|
||||
.replace(/\\\*/g, ".*") // replace wildcards by .*
|
||||
// process separator placeholders (all ANSI charaters but alphanumeric characters and _%.-)
|
||||
.replace(/\\\^/g, "(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x80]|$)")
|
||||
.replace(/^\\\|\\\|/, "^[\\w\\-]+:\\/+(?!\\/)(?:[^.\\/]+\\.)*?") // process extended anchor at expression start
|
||||
.replace(/^\\\|/, "^") // process anchor at expression start
|
||||
.replace(/\\\|$/, "$"); // process anchor at expression end
|
||||
|
||||
let regexp = new RegExp(source, this.matchCase ? "" : "i");
|
||||
|
||||
delete this.regexp;
|
||||
delete this.regexpSource;
|
||||
return (this.regexp = regexp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Tests whether the URL matches this filter
|
||||
* @param {String} location URL to be tested
|
||||
* @param {String} contentType content type identifier of the URL
|
||||
* @param {String} docDomain domain name of the document that loads the URL
|
||||
* @param {Boolean} thirdParty should be true if the URL is a third-party request
|
||||
* @return {Boolean} true in case of a match
|
||||
*/
|
||||
matches: function(location, contentType, docDomain, thirdParty)
|
||||
{
|
||||
if (this.regexp.test(location) &&
|
||||
(RegExpFilter.typeMap[contentType] & this.contentType) != 0 &&
|
||||
(this.thirdParty == null || this.thirdParty == thirdParty) &&
|
||||
this.isActiveOnDomain(docDomain))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
RegExpFilter.prototype.__defineGetter__("0", function()
|
||||
{
|
||||
return this;
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a RegExp filter from its text representation
|
||||
* @param {String} text same as in Filter()
|
||||
*/
|
||||
RegExpFilter.fromText = function(text)
|
||||
{
|
||||
let blocking = true;
|
||||
let origText = text;
|
||||
if (text.indexOf("@@") == 0)
|
||||
{
|
||||
blocking = false;
|
||||
text = text.substr(2);
|
||||
}
|
||||
|
||||
let contentType = null;
|
||||
let matchCase = null;
|
||||
let domains = null;
|
||||
let thirdParty = null;
|
||||
let collapse = null;
|
||||
let options;
|
||||
let match = Filter.optionsRegExp.exec(text);
|
||||
if (match)
|
||||
{
|
||||
options = match[1].toUpperCase().split(",");
|
||||
text = match.input.substr(0, match.index);
|
||||
for each (let option in options)
|
||||
{
|
||||
let value = null;
|
||||
let separatorIndex = option.indexOf("=");
|
||||
if (separatorIndex >= 0)
|
||||
{
|
||||
value = option.substr(separatorIndex + 1);
|
||||
option = option.substr(0, separatorIndex);
|
||||
}
|
||||
option = option.replace(/-/, "_");
|
||||
if (option in RegExpFilter.typeMap)
|
||||
{
|
||||
if (contentType == null)
|
||||
contentType = 0;
|
||||
contentType |= RegExpFilter.typeMap[option];
|
||||
}
|
||||
else if (option[0] == "~" && option.substr(1) in RegExpFilter.typeMap)
|
||||
{
|
||||
if (contentType == null)
|
||||
contentType = RegExpFilter.prototype.contentType;
|
||||
contentType &= ~RegExpFilter.typeMap[option.substr(1)];
|
||||
}
|
||||
else if (option == "MATCH_CASE")
|
||||
matchCase = true;
|
||||
else if (option == "DOMAIN" && typeof value != "undefined")
|
||||
domains = value;
|
||||
else if (option == "THIRD_PARTY")
|
||||
thirdParty = true;
|
||||
else if (option == "~THIRD_PARTY")
|
||||
thirdParty = false;
|
||||
else if (option == "COLLAPSE")
|
||||
collapse = true;
|
||||
else if (option == "~COLLAPSE")
|
||||
collapse = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!blocking && (contentType == null || (contentType & RegExpFilter.typeMap.DOCUMENT)) &&
|
||||
(!options || options.indexOf("DOCUMENT") < 0) && !/^\|?[\w\-]+:/.test(text))
|
||||
{
|
||||
// Exception filters shouldn't apply to pages by default unless they start with a protocol name
|
||||
if (contentType == null)
|
||||
contentType = RegExpFilter.prototype.contentType;
|
||||
contentType &= ~RegExpFilter.typeMap.DOCUMENT;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (blocking)
|
||||
return new BlockingFilter(origText, text, contentType, matchCase, domains, thirdParty, collapse);
|
||||
else
|
||||
return new WhitelistFilter(origText, text, contentType, matchCase, domains, thirdParty);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
return new InvalidFilter(text, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps type strings like "SCRIPT" or "OBJECT" to bit masks
|
||||
*/
|
||||
RegExpFilter.typeMap = {
|
||||
OTHER: 1,
|
||||
SCRIPT: 2,
|
||||
IMAGE: 4,
|
||||
STYLESHEET: 8,
|
||||
OBJECT: 16,
|
||||
SUBDOCUMENT: 32,
|
||||
DOCUMENT: 64,
|
||||
XBL: 1,
|
||||
PING: 1,
|
||||
XMLHTTPREQUEST: 2048,
|
||||
OBJECT_SUBREQUEST: 4096,
|
||||
DTD: 1,
|
||||
MEDIA: 16384,
|
||||
FONT: 32768,
|
||||
WEBSOCKET: 1,
|
||||
WEBRTC: 1,
|
||||
CSP: 1,
|
||||
|
||||
BACKGROUND: 4, // Backwards compat, same as IMAGE
|
||||
|
||||
POPUP: 0x10000000,
|
||||
DONOTTRACK: 0x20000000,
|
||||
ELEMHIDE: 0x40000000
|
||||
};
|
||||
|
||||
// ELEMHIDE, DONOTTRACK, POPUP option shouldn't be there by default
|
||||
RegExpFilter.prototype.contentType &= ~(
|
||||
RegExpFilter.typeMap.ELEMHIDE |
|
||||
RegExpFilter.typeMap.DONOTTRACK |
|
||||
RegExpFilter.typeMap.POPUP |
|
||||
RegExpFilter.typeMap.WEBSOCKET |
|
||||
RegExpFilter.typeMap.WEBRTC |
|
||||
RegExpFilter.typeMap.CSP
|
||||
);
|
||||
|
||||
/**
|
||||
* Class for blocking filters
|
||||
* @param {String} text see Filter()
|
||||
* @param {String} regexpSource see RegExpFilter()
|
||||
* @param {Number} contentType see RegExpFilter()
|
||||
* @param {Boolean} matchCase see RegExpFilter()
|
||||
* @param {String} domains see RegExpFilter()
|
||||
* @param {Boolean} thirdParty see RegExpFilter()
|
||||
* @param {Boolean} collapse defines whether the filter should collapse blocked content, can be null
|
||||
* @constructor
|
||||
* @augments RegExpFilter
|
||||
*/
|
||||
function BlockingFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, collapse)
|
||||
{
|
||||
RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty);
|
||||
|
||||
this.collapse = collapse;
|
||||
}
|
||||
BlockingFilter.prototype =
|
||||
{
|
||||
__proto__: RegExpFilter.prototype,
|
||||
|
||||
/**
|
||||
* Defines whether the filter should collapse blocked content. Can be null (use the global preference).
|
||||
* @type Boolean
|
||||
*/
|
||||
collapse: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Class for whitelist filters
|
||||
* @param {String} text see Filter()
|
||||
* @param {String} regexpSource see RegExpFilter()
|
||||
* @param {Number} contentType see RegExpFilter()
|
||||
* @param {Boolean} matchCase see RegExpFilter()
|
||||
* @param {String} domains see RegExpFilter()
|
||||
* @param {Boolean} thirdParty see RegExpFilter()
|
||||
* @constructor
|
||||
* @augments RegExpFilter
|
||||
*/
|
||||
function WhitelistFilter(text, regexpSource, contentType, matchCase, domains, thirdParty)
|
||||
{
|
||||
RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty);
|
||||
}
|
||||
|
||||
WhitelistFilter.prototype =
|
||||
{
|
||||
__proto__: RegExpFilter.prototype,
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for element hiding filters
|
||||
* @param {String} text see Filter()
|
||||
* @param {String} domains (optional) Host names or domains the filter should be restricted to
|
||||
* @param {String} selector CSS selector for the HTML elements that should be hidden
|
||||
* @constructor
|
||||
* @augments ActiveFilter
|
||||
*/
|
||||
function ElemHideFilter(text, domains, selector)
|
||||
{
|
||||
ActiveFilter.call(this, text, domains ? domains.toUpperCase() : null);
|
||||
|
||||
if (domains)
|
||||
this.selectorDomain = domains.replace(/,~[^,]+/g, "").replace(/^~[^,]+,?/, "").toLowerCase();
|
||||
this.selector = selector;
|
||||
}
|
||||
ElemHideFilter.prototype =
|
||||
{
|
||||
__proto__: ActiveFilter.prototype,
|
||||
|
||||
/**
|
||||
* @see ActiveFilter.domainSeparator
|
||||
*/
|
||||
domainSeparator: ",",
|
||||
|
||||
/**
|
||||
* Host name or domain the filter should be restricted to (can be null for no restriction)
|
||||
* @type String
|
||||
*/
|
||||
selectorDomain: null,
|
||||
/**
|
||||
* CSS selector for the HTML elements that should be hidden
|
||||
* @type String
|
||||
*/
|
||||
selector: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an element hiding filter from a pre-parsed text representation
|
||||
*
|
||||
* @param {String} text same as in Filter()
|
||||
* @param {String} domain domain part of the text representation (can be empty)
|
||||
* @param {String} tagName tag name part (can be empty)
|
||||
* @param {String} attrRules attribute matching rules (can be empty)
|
||||
* @param {String} selector raw CSS selector (can be empty)
|
||||
* @return {ElemHideFilter or InvalidFilter}
|
||||
*/
|
||||
ElemHideFilter.fromText = function(text, domain, tagName, attrRules, selector)
|
||||
{
|
||||
if (!selector)
|
||||
{
|
||||
if (tagName == "*")
|
||||
tagName = "";
|
||||
|
||||
let id = null;
|
||||
let additional = "";
|
||||
if (attrRules) {
|
||||
attrRules = attrRules.match(/\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\)/g);
|
||||
for each (let rule in attrRules) {
|
||||
rule = rule.substr(1, rule.length - 2);
|
||||
let separatorPos = rule.indexOf("=");
|
||||
if (separatorPos > 0) {
|
||||
rule = rule.replace(/=/, '="') + '"';
|
||||
additional += "[" + rule + "]";
|
||||
}
|
||||
else {
|
||||
if (id)
|
||||
return new InvalidFilter(text, Utils.getString("filter_elemhide_duplicate_id"));
|
||||
else
|
||||
id = rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (id)
|
||||
selector = tagName + "." + id + additional + "," + tagName + "#" + id + additional;
|
||||
else if (tagName || additional)
|
||||
selector = tagName + additional;
|
||||
else
|
||||
return new InvalidFilter(text, Utils.getString("filter_elemhide_nocriteria"));
|
||||
}
|
||||
return new ElemHideFilter(text, domain, selector);
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Component synchronizing filter storage with Matcher instances and ElemHide.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["FilterListener"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import(baseURL + "TimeLine.jsm");
|
||||
Cu.import(baseURL + "FilterStorage.jsm");
|
||||
Cu.import(baseURL + "FilterNotifier.jsm");
|
||||
Cu.import(baseURL + "ElemHide.jsm");
|
||||
Cu.import(baseURL + "Matcher.jsm");
|
||||
Cu.import(baseURL + "FilterClasses.jsm");
|
||||
Cu.import(baseURL + "SubscriptionClasses.jsm");
|
||||
Cu.import(baseURL + "Prefs.jsm");
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
|
||||
/**
|
||||
* Value of the FilterListener.batchMode property.
|
||||
* @type Boolean
|
||||
*/
|
||||
let batchMode = false;
|
||||
|
||||
/**
|
||||
* Increases on filter changes, filters will be saved if it exceeds 1.
|
||||
* @type Integer
|
||||
*/
|
||||
let isDirty = 0;
|
||||
|
||||
/**
|
||||
* This object can be used to change properties of the filter change listeners.
|
||||
* @class
|
||||
*/
|
||||
var FilterListener =
|
||||
{
|
||||
/**
|
||||
* Called on module initialization, registers listeners for FilterStorage changes
|
||||
*/
|
||||
startup: function()
|
||||
{
|
||||
TimeLine.enter("Entered FilterListener.startup()");
|
||||
|
||||
FilterNotifier.addListener(function(action, item, newValue, oldValue)
|
||||
{
|
||||
let match = /^(\w+)\.(.*)/.exec(action);
|
||||
if (match && match[1] == "filter")
|
||||
onFilterChange(match[2], item, newValue, oldValue);
|
||||
else if (match && match[1] == "subscription")
|
||||
onSubscriptionChange(match[2], item, newValue, oldValue);
|
||||
else
|
||||
onGenericChange(action, item);
|
||||
});
|
||||
|
||||
ElemHide.init();
|
||||
FilterStorage.loadFromDisk();
|
||||
|
||||
TimeLine.log("done initializing data structures");
|
||||
|
||||
Services.obs.addObserver(FilterListenerPrivate, "browser:purge-session-history", true);
|
||||
TimeLine.log("done adding observers");
|
||||
|
||||
TimeLine.leave("FilterListener.startup() done");
|
||||
},
|
||||
|
||||
/**
|
||||
* Set to true when executing many changes, changes will only be fully applied after this variable is set to false again.
|
||||
* @type Boolean
|
||||
*/
|
||||
get batchMode()
|
||||
{
|
||||
return batchMode;
|
||||
},
|
||||
set batchMode(value)
|
||||
{
|
||||
batchMode = value;
|
||||
flushElemHide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Increases "dirty factor" of the filters and calls FilterStorage.saveToDisk()
|
||||
* if it becomes 1 or more. Save is executed delayed to prevent multiple
|
||||
* subsequent calls. If the parameter is 0 it forces saving filters if any
|
||||
* changes were recorded after the previous save.
|
||||
*/
|
||||
setDirty: function(/**Integer*/ factor)
|
||||
{
|
||||
if (factor == 0 && isDirty > 0)
|
||||
isDirty = 1;
|
||||
else
|
||||
isDirty += factor;
|
||||
if (isDirty >= 1)
|
||||
FilterStorage.saveToDisk();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Private nsIObserver implementation.
|
||||
* @class
|
||||
*/
|
||||
var FilterListenerPrivate =
|
||||
{
|
||||
observe: function(subject, topic, data)
|
||||
{
|
||||
if (topic == "browser:purge-session-history" && Prefs.clearStatsOnHistoryPurge)
|
||||
{
|
||||
FilterStorage.resetHitCounts();
|
||||
FilterListener.setDirty(0); // Force saving to disk
|
||||
|
||||
Prefs.recentReports = "[]";
|
||||
}
|
||||
},
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver])
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls ElemHide.apply() if necessary.
|
||||
*/
|
||||
function flushElemHide()
|
||||
{
|
||||
if (!batchMode && ElemHide.isDirty)
|
||||
ElemHide.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies Matcher instances or ElemHide object about a new filter
|
||||
* if necessary.
|
||||
* @param {Filter} filter filter that has been added
|
||||
*/
|
||||
function addFilter(filter)
|
||||
{
|
||||
if (!(filter instanceof ActiveFilter) || filter.disabled)
|
||||
return;
|
||||
|
||||
let hasEnabled = false;
|
||||
for (let i = 0; i < filter.subscriptions.length; i++)
|
||||
if (!filter.subscriptions[i].disabled)
|
||||
hasEnabled = true;
|
||||
if (!hasEnabled)
|
||||
return;
|
||||
|
||||
if (filter instanceof RegExpFilter)
|
||||
defaultMatcher.add(filter);
|
||||
else if (filter instanceof ElemHideFilter)
|
||||
ElemHide.add(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies Matcher instances or ElemHide object about removal of a filter
|
||||
* if necessary.
|
||||
* @param {Filter} filter filter that has been removed
|
||||
*/
|
||||
function removeFilter(filter)
|
||||
{
|
||||
if (!(filter instanceof ActiveFilter))
|
||||
return;
|
||||
|
||||
if (!filter.disabled)
|
||||
{
|
||||
let hasEnabled = false;
|
||||
for (let i = 0; i < filter.subscriptions.length; i++)
|
||||
if (!filter.subscriptions[i].disabled)
|
||||
hasEnabled = true;
|
||||
if (hasEnabled)
|
||||
return;
|
||||
}
|
||||
|
||||
if (filter instanceof RegExpFilter)
|
||||
defaultMatcher.remove(filter);
|
||||
else if (filter instanceof ElemHideFilter)
|
||||
ElemHide.remove(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription change listener
|
||||
*/
|
||||
function onSubscriptionChange(action, subscription, newValue, oldValue)
|
||||
{
|
||||
FilterListener.setDirty(1);
|
||||
|
||||
if (action != "added" && action != "removed" && action != "disabled" && action != "updated")
|
||||
return;
|
||||
|
||||
if (action != "removed" && !(subscription.url in FilterStorage.knownSubscriptions))
|
||||
{
|
||||
// Ignore updates for subscriptions not in the list
|
||||
return;
|
||||
}
|
||||
|
||||
if ((action == "added" || action == "removed" || action == "updated") && subscription.disabled)
|
||||
{
|
||||
// Ignore adding/removing/updating of disabled subscriptions
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "added" || action == "removed" || action == "disabled")
|
||||
{
|
||||
let method = (action == "added" || (action == "disabled" && newValue == false) ? addFilter : removeFilter);
|
||||
if (subscription.filters)
|
||||
subscription.filters.forEach(method);
|
||||
}
|
||||
else if (action == "updated")
|
||||
{
|
||||
subscription.oldFilters.forEach(removeFilter);
|
||||
subscription.filters.forEach(addFilter);
|
||||
}
|
||||
|
||||
flushElemHide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter change listener
|
||||
*/
|
||||
function onFilterChange(action, filter, newValue, oldValue)
|
||||
{
|
||||
if (action == "hitCount" || action == "lastHit")
|
||||
FilterListener.setDirty(0.002);
|
||||
else
|
||||
FilterListener.setDirty(1);
|
||||
|
||||
if (action != "added" && action != "removed" && action != "disabled")
|
||||
return;
|
||||
|
||||
if ((action == "added" || action == "removed") && filter.disabled)
|
||||
{
|
||||
// Ignore adding/removing of disabled filters
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "added" || (action == "disabled" && newValue == false))
|
||||
addFilter(filter);
|
||||
else
|
||||
removeFilter(filter);
|
||||
flushElemHide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic notification listener
|
||||
*/
|
||||
function onGenericChange(action)
|
||||
{
|
||||
if (action == "load")
|
||||
{
|
||||
isDirty = 0;
|
||||
|
||||
defaultMatcher.clear();
|
||||
ElemHide.clear();
|
||||
for each (let subscription in FilterStorage.subscriptions)
|
||||
if (!subscription.disabled)
|
||||
subscription.filters.forEach(addFilter);
|
||||
flushElemHide();
|
||||
}
|
||||
else if (action == "save")
|
||||
isDirty = 0;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview FilterNotifier class manages listeners and distributes messages
|
||||
* about filter changes to them.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["FilterNotifier"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
/**
|
||||
* List of registered listeners
|
||||
* @type Array of function(action, item, newValue, oldValue)
|
||||
*/
|
||||
let listeners = [];
|
||||
|
||||
/**
|
||||
* This class allows registering and triggering listeners for filter events.
|
||||
* @class
|
||||
*/
|
||||
var FilterNotifier =
|
||||
{
|
||||
/**
|
||||
* Adds a listener
|
||||
*/
|
||||
addListener: function(/**function(action, item, newValue, oldValue)*/ listener)
|
||||
{
|
||||
if (listeners.indexOf(listener) >= 0)
|
||||
return;
|
||||
|
||||
listeners.push(listener);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a listener that was previosly added via addListener
|
||||
*/
|
||||
removeListener: function(/**function(action, item, newValue, oldValue)*/ listener)
|
||||
{
|
||||
let index = listeners.indexOf(listener);
|
||||
if (index >= 0)
|
||||
listeners.splice(index, 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies listeners about an event
|
||||
* @param {String} action event code ("load", "save", "elemhideupdate",
|
||||
* "subscription.added", "subscription.removed",
|
||||
* "subscription.disabled", "subscription.title",
|
||||
* "subscription.lastDownload", "subscription.downloadStatus",
|
||||
* "subscription.homepage", "subscription.updated",
|
||||
* "filter.added", "filter.removed", "filter.moved",
|
||||
* "filter.disabled", "filter.hitCount", "filter.lastHit")
|
||||
* @param {Subscription|Filter} item item that the change applies to
|
||||
*/
|
||||
triggerListeners: function(action, item, param1, param2, param3)
|
||||
{
|
||||
for each (let listener in listeners)
|
||||
listener(action, item, param1, param2, param3);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,776 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview FilterStorage class responsible to managing user's subscriptions and filters.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["FilterStorage"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
Cu.import(baseURL + "IO.jsm");
|
||||
Cu.import(baseURL + "Prefs.jsm");
|
||||
Cu.import(baseURL + "FilterClasses.jsm");
|
||||
Cu.import(baseURL + "SubscriptionClasses.jsm");
|
||||
Cu.import(baseURL + "FilterNotifier.jsm");
|
||||
Cu.import(baseURL + "TimeLine.jsm");
|
||||
|
||||
/**
|
||||
* Version number of the filter storage file format.
|
||||
* @type Integer
|
||||
*/
|
||||
const formatVersion = 4;
|
||||
|
||||
/**
|
||||
* This class reads user's filters from disk, manages them in memory and writes them back.
|
||||
* @class
|
||||
*/
|
||||
var FilterStorage =
|
||||
{
|
||||
/**
|
||||
* Version number of the patterns.ini format used.
|
||||
* @type Integer
|
||||
*/
|
||||
get formatVersion() formatVersion,
|
||||
|
||||
/**
|
||||
* File that the filter list has been loaded from and should be saved to
|
||||
* @type nsIFile
|
||||
*/
|
||||
get sourceFile()
|
||||
{
|
||||
let file = null;
|
||||
if (Prefs.patternsfile)
|
||||
{
|
||||
// Override in place, use it instead of placing the file in the regular data dir
|
||||
file = IO.resolveFilePath(Prefs.patternsfile);
|
||||
}
|
||||
if (!file)
|
||||
{
|
||||
// Place the file in the data dir
|
||||
file = IO.resolveFilePath(Prefs.data_directory);
|
||||
if (file)
|
||||
file.append("patterns.ini");
|
||||
}
|
||||
if (!file)
|
||||
{
|
||||
// Data directory pref misconfigured? Try the default value
|
||||
try
|
||||
{
|
||||
file = IO.resolveFilePath(Prefs.defaultBranch.getCharPref("data_directory"));
|
||||
if (file)
|
||||
file.append("patterns.ini");
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
if (!file)
|
||||
Cu.reportError("Adblock Plus: Failed to resolve filter file location from extensions.@ADDON_CHROME_NAME@.patternsfile preference");
|
||||
|
||||
this.__defineGetter__("sourceFile", function() file);
|
||||
return this.sourceFile;
|
||||
},
|
||||
|
||||
/**
|
||||
* Map of properties listed in the filter storage file before the sections
|
||||
* start. Right now this should be only the format version.
|
||||
*/
|
||||
fileProperties: {__proto__: null},
|
||||
|
||||
/**
|
||||
* List of filter subscriptions containing all filters
|
||||
* @type Array of Subscription
|
||||
*/
|
||||
subscriptions: [],
|
||||
|
||||
/**
|
||||
* Map of subscriptions already on the list, by their URL/identifier
|
||||
* @type Object
|
||||
*/
|
||||
knownSubscriptions: {__proto__: null},
|
||||
|
||||
/**
|
||||
* Finds the filter group that a filter should be added to by default. Will
|
||||
* return null if this group doesn't exist yet.
|
||||
*/
|
||||
getGroupForFilter: function(/**Filter*/ filter) /**SpecialSubscription*/
|
||||
{
|
||||
let generalSubscription = null;
|
||||
for each (let subscription in FilterStorage.subscriptions)
|
||||
{
|
||||
if (subscription instanceof SpecialSubscription && !subscription.disabled)
|
||||
{
|
||||
// Always prefer specialized subscriptions
|
||||
if (subscription.isDefaultFor(filter))
|
||||
return subscription;
|
||||
|
||||
// If this is a general subscription - store it as fallback
|
||||
if (!generalSubscription && (!subscription.defaults || !subscription.defaults.length))
|
||||
generalSubscription = subscription;
|
||||
}
|
||||
}
|
||||
return generalSubscription;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a filter subscription to the list
|
||||
* @param {Subscription} subscription filter subscription to be added
|
||||
* @param {Boolean} silent if true, no listeners will be triggered (to be used when filter list is reloaded)
|
||||
*/
|
||||
addSubscription: function(subscription, silent)
|
||||
{
|
||||
if (subscription.url in FilterStorage.knownSubscriptions)
|
||||
return;
|
||||
|
||||
FilterStorage.subscriptions.push(subscription);
|
||||
FilterStorage.knownSubscriptions[subscription.url] = subscription;
|
||||
addSubscriptionFilters(subscription);
|
||||
|
||||
if (!silent)
|
||||
FilterNotifier.triggerListeners("subscription.added", subscription);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a filter subscription from the list
|
||||
* @param {Subscription} subscription filter subscription to be removed
|
||||
* @param {Boolean} silent if true, no listeners will be triggered (to be used when filter list is reloaded)
|
||||
*/
|
||||
removeSubscription: function(subscription, silent)
|
||||
{
|
||||
for (let i = 0; i < FilterStorage.subscriptions.length; i++)
|
||||
{
|
||||
if (FilterStorage.subscriptions[i].url == subscription.url)
|
||||
{
|
||||
removeSubscriptionFilters(subscription);
|
||||
|
||||
FilterStorage.subscriptions.splice(i--, 1);
|
||||
delete FilterStorage.knownSubscriptions[subscription.url];
|
||||
if (!silent)
|
||||
FilterNotifier.triggerListeners("subscription.removed", subscription);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves a subscription in the list to a new position.
|
||||
* @param {Subscription} subscription filter subscription to be moved
|
||||
* @param {Subscription} [insertBefore] filter subscription to insert before
|
||||
* (if omitted the subscription will be put at the end of the list)
|
||||
*/
|
||||
moveSubscription: function(subscription, insertBefore)
|
||||
{
|
||||
let currentPos = FilterStorage.subscriptions.indexOf(subscription);
|
||||
if (currentPos < 0)
|
||||
return;
|
||||
|
||||
let newPos = insertBefore ? FilterStorage.subscriptions.indexOf(insertBefore) : -1;
|
||||
if (newPos < 0)
|
||||
newPos = FilterStorage.subscriptions.length;
|
||||
|
||||
if (currentPos < newPos)
|
||||
newPos--;
|
||||
if (currentPos == newPos)
|
||||
return;
|
||||
|
||||
FilterStorage.subscriptions.splice(currentPos, 1);
|
||||
FilterStorage.subscriptions.splice(newPos, 0, subscription);
|
||||
FilterNotifier.triggerListeners("subscription.moved", subscription);
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces the list of filters in a subscription by a new list
|
||||
* @param {Subscription} subscription filter subscription to be updated
|
||||
* @param {Array of Filter} filters new filter lsit
|
||||
*/
|
||||
updateSubscriptionFilters: function(subscription, filters)
|
||||
{
|
||||
removeSubscriptionFilters(subscription);
|
||||
subscription.oldFilters = subscription.filters;
|
||||
subscription.filters = filters;
|
||||
addSubscriptionFilters(subscription);
|
||||
FilterNotifier.triggerListeners("subscription.updated", subscription);
|
||||
delete subscription.oldFilters;
|
||||
|
||||
// Do not keep empty subscriptions disabled
|
||||
if (subscription instanceof SpecialSubscription && !subscription.filters.length && subscription.disabled)
|
||||
subscription.disabled = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a user-defined filter to the list
|
||||
* @param {Filter} filter
|
||||
* @param {SpecialSubscription} [subscription] particular group that the filter should be added to
|
||||
* @param {Integer} [position] position within the subscription at which the filter should be added
|
||||
* @param {Boolean} silent if true, no listeners will be triggered (to be used when filter list is reloaded)
|
||||
*/
|
||||
addFilter: function(filter, subscription, position, silent)
|
||||
{
|
||||
if (!subscription)
|
||||
{
|
||||
if (filter.subscriptions.some(function(s) s instanceof SpecialSubscription && !s.disabled))
|
||||
return; // No need to add
|
||||
subscription = FilterStorage.getGroupForFilter(filter);
|
||||
}
|
||||
if (!subscription)
|
||||
{
|
||||
// No group for this filter exists, create one
|
||||
subscription = SpecialSubscription.createForFilter(filter);
|
||||
this.addSubscription(subscription);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof position == "undefined")
|
||||
position = subscription.filters.length;
|
||||
|
||||
if (filter.subscriptions.indexOf(subscription) < 0)
|
||||
filter.subscriptions.push(subscription);
|
||||
subscription.filters.splice(position, 0, filter);
|
||||
if (!silent)
|
||||
FilterNotifier.triggerListeners("filter.added", filter, subscription, position);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a user-defined filter from the list
|
||||
* @param {Filter} filter
|
||||
* @param {SpecialSubscription} [subscription] a particular filter group that
|
||||
* the filter should be removed from (if ommited will be removed from all subscriptions)
|
||||
* @param {Integer} [position] position inside the filter group at which the
|
||||
* filter should be removed (if ommited all instances will be removed)
|
||||
*/
|
||||
removeFilter: function(filter, subscription, position)
|
||||
{
|
||||
let subscriptions = (subscription ? [subscription] : filter.subscriptions.slice());
|
||||
for (let i = 0; i < subscriptions.length; i++)
|
||||
{
|
||||
let subscription = subscriptions[i];
|
||||
if (subscription instanceof SpecialSubscription)
|
||||
{
|
||||
let positions = [];
|
||||
if (typeof position == "undefined")
|
||||
{
|
||||
let index = -1;
|
||||
do
|
||||
{
|
||||
index = subscription.filters.indexOf(filter, index + 1);
|
||||
if (index >= 0)
|
||||
positions.push(index);
|
||||
} while (index >= 0);
|
||||
}
|
||||
else
|
||||
positions.push(position);
|
||||
|
||||
for (let j = positions.length - 1; j >= 0; j--)
|
||||
{
|
||||
let position = positions[j];
|
||||
if (subscription.filters[position] == filter)
|
||||
{
|
||||
subscription.filters.splice(position, 1);
|
||||
if (subscription.filters.indexOf(filter) < 0)
|
||||
{
|
||||
let index = filter.subscriptions.indexOf(subscription);
|
||||
if (index >= 0)
|
||||
filter.subscriptions.splice(index, 1);
|
||||
}
|
||||
FilterNotifier.triggerListeners("filter.removed", filter, subscription, position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves a user-defined filter to a new position
|
||||
* @param {Filter} filter
|
||||
* @param {SpecialSubscription} subscription filter group where the filter is located
|
||||
* @param {Integer} oldPosition current position of the filter
|
||||
* @param {Integer} newPosition new position of the filter
|
||||
*/
|
||||
moveFilter: function(filter, subscription, oldPosition, newPosition)
|
||||
{
|
||||
if (!(subscription instanceof SpecialSubscription) || subscription.filters[oldPosition] != filter)
|
||||
return;
|
||||
|
||||
newPosition = Math.min(Math.max(newPosition, 0), subscription.filters.length - 1);
|
||||
if (oldPosition == newPosition)
|
||||
return;
|
||||
|
||||
subscription.filters.splice(oldPosition, 1);
|
||||
subscription.filters.splice(newPosition, 0, filter);
|
||||
FilterNotifier.triggerListeners("filter.moved", filter, subscription, oldPosition, newPosition);
|
||||
},
|
||||
|
||||
/**
|
||||
* Increases the hit count for a filter by one
|
||||
* @param {Filter} filter
|
||||
*/
|
||||
increaseHitCount: function(filter)
|
||||
{
|
||||
if (!Prefs.savestats || Prefs.privateBrowsing || !(filter instanceof ActiveFilter))
|
||||
return;
|
||||
|
||||
filter.hitCount++;
|
||||
filter.lastHit = Date.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets hit count for some filters
|
||||
* @param {Array of Filter} filters filters to be reset, if null all filters will be reset
|
||||
*/
|
||||
resetHitCounts: function(filters)
|
||||
{
|
||||
if (!filters)
|
||||
{
|
||||
filters = [];
|
||||
for each (let filter in Filter.knownFilters)
|
||||
filters.push(filter);
|
||||
}
|
||||
for each (let filter in filters)
|
||||
{
|
||||
filter.hitCount = 0;
|
||||
filter.lastHit = 0;
|
||||
}
|
||||
},
|
||||
|
||||
_loading: false,
|
||||
|
||||
/**
|
||||
* Loads all subscriptions from the disk
|
||||
* @param {nsIFile} [sourceFile] File to read from
|
||||
*/
|
||||
loadFromDisk: function(sourceFile)
|
||||
{
|
||||
if (this._loading)
|
||||
return;
|
||||
|
||||
TimeLine.enter("Entered FilterStorage.loadFromDisk()");
|
||||
|
||||
let explicitFile = true;
|
||||
if (!sourceFile)
|
||||
{
|
||||
sourceFile = FilterStorage.sourceFile;
|
||||
explicitFile = false;
|
||||
|
||||
if (!sourceFile || !sourceFile.exists())
|
||||
sourceFile = Utils.makeURI("resource://@ADDON_CHROME_NAME@/defaults/patterns.ini");
|
||||
}
|
||||
|
||||
let readFile = function(sourceFile, backupIndex)
|
||||
{
|
||||
TimeLine.enter("FilterStorage.loadFromDisk() -> readFile()");
|
||||
|
||||
let parser = new INIParser();
|
||||
IO.readFromFile(sourceFile, true, parser, function(e)
|
||||
{
|
||||
TimeLine.enter("FilterStorage.loadFromDisk() read callback");
|
||||
if (!e && parser.subscriptions.length == 0)
|
||||
{
|
||||
// No filter subscriptions in the file, this isn't right.
|
||||
e = new Error("No data in the file");
|
||||
}
|
||||
|
||||
if (e)
|
||||
Cu.reportError(e);
|
||||
|
||||
if (e && !explicitFile)
|
||||
{
|
||||
// Attempt to load a backup
|
||||
sourceFile = this.sourceFile;
|
||||
if (sourceFile)
|
||||
{
|
||||
let [, part1, part2] = /^(.*)(\.\w+)$/.exec(sourceFile.leafName) || [null, sourceFile.leafName, ""];
|
||||
|
||||
sourceFile = sourceFile.clone();
|
||||
sourceFile.leafName = part1 + "-backup" + (++backupIndex) + part2;
|
||||
|
||||
if (sourceFile.exists())
|
||||
{
|
||||
readFile(sourceFile, backupIndex);
|
||||
TimeLine.leave("FilterStorage.loadFromDisk() read callback done");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Old special groups might have been converted, remove them if they are empty
|
||||
let specialMap = {"~il~": true, "~wl~": true, "~fl~": true, "~eh~": true};
|
||||
let knownSubscriptions = {__proto__: null};
|
||||
for (let i = 0; i < parser.subscriptions.length; i++)
|
||||
{
|
||||
let subscription = parser.subscriptions[i];
|
||||
if (subscription instanceof SpecialSubscription && subscription.filters.length == 0 && subscription.url in specialMap)
|
||||
parser.subscriptions.splice(i--, 1);
|
||||
else
|
||||
knownSubscriptions[subscription.url] = subscription;
|
||||
}
|
||||
|
||||
this.fileProperties = parser.fileProperties;
|
||||
this.subscriptions = parser.subscriptions;
|
||||
this.knownSubscriptions = knownSubscriptions;
|
||||
Filter.knownFilters = parser.knownFilters;
|
||||
Subscription.knownSubscriptions = parser.knownSubscriptions;
|
||||
|
||||
if (parser.userFilters)
|
||||
{
|
||||
for (let i = 0; i < parser.userFilters.length; i++)
|
||||
{
|
||||
let filter = Filter.fromText(parser.userFilters[i]);
|
||||
if (filter)
|
||||
this.addFilter(filter, null, undefined, true);
|
||||
}
|
||||
}
|
||||
TimeLine.log("Initializing data done, triggering observers")
|
||||
|
||||
this._loading = false;
|
||||
FilterNotifier.triggerListeners("load");
|
||||
|
||||
if (sourceFile != this.sourceFile)
|
||||
this.saveToDisk();
|
||||
|
||||
TimeLine.leave("FilterStorage.loadFromDisk() read callback done");
|
||||
}.bind(this), "FilterStorageRead");
|
||||
|
||||
TimeLine.leave("FilterStorage.loadFromDisk() <- readFile()", "FilterStorageRead");
|
||||
}.bind(this);
|
||||
|
||||
this._loading = true;
|
||||
readFile(sourceFile, 0);
|
||||
|
||||
TimeLine.leave("FilterStorage.loadFromDisk() done");
|
||||
},
|
||||
|
||||
_generateFilterData: function(subscriptions)
|
||||
{
|
||||
yield "# Adblock Plus preferences";
|
||||
yield "version=" + formatVersion;
|
||||
|
||||
let saved = {__proto__: null};
|
||||
let buf = [];
|
||||
|
||||
// Save filter data
|
||||
for (let i = 0; i < subscriptions.length; i++)
|
||||
{
|
||||
let subscription = subscriptions[i];
|
||||
for (let j = 0; j < subscription.filters.length; j++)
|
||||
{
|
||||
let filter = subscription.filters[j];
|
||||
if (!(filter.text in saved))
|
||||
{
|
||||
filter.serialize(buf);
|
||||
saved[filter.text] = filter;
|
||||
for (let k = 0; k < buf.length; k++)
|
||||
yield buf[k];
|
||||
buf.splice(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save subscriptions
|
||||
for (let i = 0; i < subscriptions.length; i++)
|
||||
{
|
||||
let subscription = subscriptions[i];
|
||||
|
||||
yield "";
|
||||
|
||||
subscription.serialize(buf);
|
||||
if (subscription.filters.length)
|
||||
{
|
||||
buf.push("", "[Subscription filters]")
|
||||
subscription.serializeFilters(buf);
|
||||
}
|
||||
for (let k = 0; k < buf.length; k++)
|
||||
yield buf[k];
|
||||
buf.splice(0);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Will be set to true if saveToDisk() is running (reentrance protection).
|
||||
* @type Boolean
|
||||
*/
|
||||
_saving: false,
|
||||
|
||||
/**
|
||||
* Will be set to true if a saveToDisk() call arrives while saveToDisk() is
|
||||
* already running (delayed execution).
|
||||
* @type Boolean
|
||||
*/
|
||||
_needsSave: false,
|
||||
|
||||
/**
|
||||
* Saves all subscriptions back to disk
|
||||
* @param {nsIFile} [targetFile] File to be written
|
||||
*/
|
||||
saveToDisk: function(targetFile)
|
||||
{
|
||||
let explicitFile = true;
|
||||
if (!targetFile)
|
||||
{
|
||||
targetFile = FilterStorage.sourceFile;
|
||||
explicitFile = false;
|
||||
}
|
||||
if (!targetFile)
|
||||
return;
|
||||
|
||||
if (!explicitFile && this._saving)
|
||||
{
|
||||
this._needsSave = true;
|
||||
return;
|
||||
}
|
||||
|
||||
TimeLine.enter("Entered FilterStorage.saveToDisk()");
|
||||
|
||||
try {
|
||||
targetFile.normalize();
|
||||
} catch (e) {}
|
||||
|
||||
// Make sure the file's parent directory exists
|
||||
try {
|
||||
targetFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
|
||||
} catch (e) {}
|
||||
|
||||
let backupFileParts = null;
|
||||
if (!explicitFile && targetFile.exists() && Prefs.patternsbackups > 0)
|
||||
{
|
||||
// Check whether we need to backup the file
|
||||
let [, part1, part2] = /^(.*)(\.\w+)$/.exec(targetFile.leafName) || [null, targetFile.leafName, ""];
|
||||
|
||||
let newestBackup = targetFile.clone();
|
||||
newestBackup.leafName = part1 + "-backup1" + part2;
|
||||
if (!newestBackup.exists() || (Date.now() - newestBackup.lastModifiedTime) / 3600000 >= Prefs.patternsbackupinterval)
|
||||
backupFileParts = [part1 + "-backup", part2];
|
||||
}
|
||||
|
||||
let writeFilters = function()
|
||||
{
|
||||
TimeLine.enter("FilterStorage.saveToDisk() -> writeFilters()");
|
||||
IO.writeToFile(targetFile, true, this._generateFilterData(subscriptions), function(e)
|
||||
{
|
||||
TimeLine.enter("FilterStorage.saveToDisk() write callback");
|
||||
if (!explicitFile)
|
||||
this._saving = false;
|
||||
|
||||
if (e)
|
||||
reportError(e);
|
||||
|
||||
if (!explicitFile && this._needsSave)
|
||||
{
|
||||
this._needsSave = false;
|
||||
this.saveToDisk();
|
||||
}
|
||||
else
|
||||
FilterNotifier.triggerListeners("save");
|
||||
TimeLine.leave("FilterStorage.saveToDisk() write callback done");
|
||||
}.bind(this), "FilterStorageWrite");
|
||||
TimeLine.leave("FilterStorage.saveToDisk() -> writeFilters()", "FilterStorageWrite");
|
||||
}.bind(this);
|
||||
|
||||
let removeLastBackup = function()
|
||||
{
|
||||
TimeLine.enter("FilterStorage.saveToDisk() -> removeLastBackup()");
|
||||
let file = targetFile.clone();
|
||||
file.leafName = backupFileParts.join(Prefs.patternsbackups);
|
||||
IO.removeFile(file, function(e) renameBackup(Prefs.patternsbackups - 1));
|
||||
TimeLine.leave("FilterStorage.saveToDisk() <- removeLastBackup()");
|
||||
}.bind(this);
|
||||
|
||||
let renameBackup = function(index)
|
||||
{
|
||||
TimeLine.enter("FilterStorage.saveToDisk() -> renameBackup()");
|
||||
if (index > 0)
|
||||
{
|
||||
let fromFile = targetFile.clone();
|
||||
fromFile.leafName = backupFileParts.join(index);
|
||||
|
||||
let toName = backupFileParts.join(index + 1);
|
||||
|
||||
IO.renameFile(fromFile, toName, function(e) renameBackup(index - 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
let toFile = targetFile.clone();
|
||||
toFile.leafName = backupFileParts.join(index + 1);
|
||||
|
||||
IO.copyFile(targetFile, toFile, writeFilters);
|
||||
}
|
||||
TimeLine.leave("FilterStorage.saveToDisk() <- renameBackup()");
|
||||
}.bind(this);
|
||||
|
||||
// Do not persist external subscriptions
|
||||
let subscriptions = this.subscriptions.filter(function(s) !(s instanceof ExternalSubscription));
|
||||
if (!explicitFile)
|
||||
this._saving = true;
|
||||
|
||||
if (backupFileParts)
|
||||
removeLastBackup();
|
||||
else
|
||||
writeFilters();
|
||||
|
||||
TimeLine.leave("FilterStorage.saveToDisk() done");
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the list of existing backup files.
|
||||
*/
|
||||
getBackupFiles: function() /**nsIFile[]*/
|
||||
{
|
||||
let result = [];
|
||||
|
||||
let [, part1, part2] = /^(.*)(\.\w+)$/.exec(FilterStorage.sourceFile.leafName) || [null, FilterStorage.sourceFile.leafName, ""];
|
||||
for (let i = 1; ; i++)
|
||||
{
|
||||
let file = FilterStorage.sourceFile.clone();
|
||||
file.leafName = part1 + "-backup" + i + part2;
|
||||
if (file.exists())
|
||||
result.push(file);
|
||||
else
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Joins subscription's filters to the subscription without any notifications.
|
||||
* @param {Subscription} subscription filter subscription that should be connected to its filters
|
||||
*/
|
||||
function addSubscriptionFilters(subscription)
|
||||
{
|
||||
if (!(subscription.url in FilterStorage.knownSubscriptions))
|
||||
return;
|
||||
|
||||
for each (let filter in subscription.filters)
|
||||
filter.subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes subscription's filters from the subscription without any notifications.
|
||||
* @param {Subscription} subscription filter subscription to be removed
|
||||
*/
|
||||
function removeSubscriptionFilters(subscription)
|
||||
{
|
||||
if (!(subscription.url in FilterStorage.knownSubscriptions))
|
||||
return;
|
||||
|
||||
for each (let filter in subscription.filters)
|
||||
{
|
||||
let i = filter.subscriptions.indexOf(subscription);
|
||||
if (i >= 0)
|
||||
filter.subscriptions.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IO.readFromFile() listener to parse filter data.
|
||||
*/
|
||||
function INIParser()
|
||||
{
|
||||
this.fileProperties = this.curObj = {};
|
||||
this.subscriptions = [];
|
||||
this.knownFilters = {__proto__: null};
|
||||
this.knownSubscriptions = {__proto__: null};
|
||||
}
|
||||
INIParser.prototype =
|
||||
{
|
||||
subscriptions: null,
|
||||
knownFilters: null,
|
||||
knownSubscrptions : null,
|
||||
wantObj: true,
|
||||
fileProperties: null,
|
||||
curObj: null,
|
||||
curSection: null,
|
||||
userFilters: null,
|
||||
|
||||
process: function(val)
|
||||
{
|
||||
let origKnownFilters = Filter.knownFilters;
|
||||
Filter.knownFilters = this.knownFilters;
|
||||
let origKnownSubscriptions = Subscription.knownSubscriptions;
|
||||
Subscription.knownSubscriptions = this.knownSubscriptions;
|
||||
let match;
|
||||
try
|
||||
{
|
||||
if (this.wantObj === true && (match = /^(\w+)=(.*)$/.exec(val)))
|
||||
this.curObj[match[1]] = match[2];
|
||||
else if (val === null || (match = /^\s*\[(.+)\]\s*$/.exec(val)))
|
||||
{
|
||||
if (this.curObj)
|
||||
{
|
||||
// Process current object before going to next section
|
||||
switch (this.curSection)
|
||||
{
|
||||
case "filter":
|
||||
case "pattern":
|
||||
if ("text" in this.curObj)
|
||||
Filter.fromObject(this.curObj);
|
||||
break;
|
||||
case "subscription":
|
||||
let subscription = Subscription.fromObject(this.curObj);
|
||||
if (subscription)
|
||||
this.subscriptions.push(subscription);
|
||||
break;
|
||||
case "subscription filters":
|
||||
case "subscription patterns":
|
||||
if (this.subscriptions.length)
|
||||
{
|
||||
let subscription = this.subscriptions[this.subscriptions.length - 1];
|
||||
for each (let text in this.curObj)
|
||||
{
|
||||
let filter = Filter.fromText(text);
|
||||
if (filter)
|
||||
{
|
||||
subscription.filters.push(filter);
|
||||
filter.subscriptions.push(subscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "user patterns":
|
||||
this.userFilters = this.curObj;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (val === null)
|
||||
return;
|
||||
|
||||
this.curSection = match[1].toLowerCase();
|
||||
switch (this.curSection)
|
||||
{
|
||||
case "filter":
|
||||
case "pattern":
|
||||
case "subscription":
|
||||
this.wantObj = true;
|
||||
this.curObj = {};
|
||||
break;
|
||||
case "subscription filters":
|
||||
case "subscription patterns":
|
||||
case "user patterns":
|
||||
this.wantObj = false;
|
||||
this.curObj = [];
|
||||
break;
|
||||
default:
|
||||
this.wantObj = undefined;
|
||||
this.curObj = null;
|
||||
}
|
||||
}
|
||||
else if (this.wantObj === false && val)
|
||||
this.curObj.push(val.replace(/\\\[/g, "["));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Filter.knownFilters = origKnownFilters;
|
||||
Subscription.knownSubscriptions = origKnownSubscriptions;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Module containing file I/O helpers.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["IO"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "TimeLine.jsm");
|
||||
|
||||
var IO =
|
||||
{
|
||||
/**
|
||||
* Retrieves the platform-dependent line break string.
|
||||
*/
|
||||
get lineBreak()
|
||||
{
|
||||
let lineBreak = (Services.appinfo.OS == "WINNT" ? "\r\n" : "\n");
|
||||
delete IO.lineBreak;
|
||||
IO.__defineGetter__("lineBreak", function() lineBreak);
|
||||
return IO.lineBreak;
|
||||
},
|
||||
|
||||
/**
|
||||
* Tries to interpret a file path as an absolute path or a path relative to
|
||||
* user's profile. Returns a file or null on failure.
|
||||
*/
|
||||
resolveFilePath: function(/**String*/ path) /**nsIFile*/
|
||||
{
|
||||
if (!path)
|
||||
return null;
|
||||
|
||||
try {
|
||||
// Assume an absolute path first
|
||||
return new FileUtils.File(path);
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
// Try relative path now
|
||||
return FileUtils.getFile("ProfD", path.split("/"));
|
||||
} catch (e) {}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reads strings from a file asynchronously, calls listener.process() with
|
||||
* each line read and with a null parameter once the read operation is done.
|
||||
* The callback will be called when the operation is done.
|
||||
*/
|
||||
readFromFile: function(/**nsIFile|nsIURI*/ file, /**Boolean*/ decode, /**Object*/ listener, /**Function*/ callback, /**String*/ timeLineID)
|
||||
{
|
||||
try
|
||||
{
|
||||
let uri = file instanceof Ci.nsIFile ? Services.io.newFileURI(file) : file;
|
||||
let channel = Services.io.newChannelFromURI(uri);
|
||||
let converter = null;
|
||||
if (decode)
|
||||
{
|
||||
converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "utf-8";
|
||||
}
|
||||
|
||||
channel.asyncOpen({
|
||||
buffer: "",
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver, Ci.nsIStreamListener]),
|
||||
onStartRequest: function(request, context) {},
|
||||
onDataAvailable: function(request, context, stream, offset, count)
|
||||
{
|
||||
if (timeLineID)
|
||||
{
|
||||
TimeLine.asyncStart(timeLineID);
|
||||
}
|
||||
|
||||
let data = this.buffer + NetUtil.readInputStreamToString(stream, count);
|
||||
let index = Math.max(data.lastIndexOf("\n"), data.lastIndexOf("\r"));
|
||||
if (index >= 0)
|
||||
{
|
||||
this.buffer = data.substr(index + 1);
|
||||
data = data.substr(0, index + 1);
|
||||
if (converter)
|
||||
data = converter.ConvertToUnicode(data);
|
||||
|
||||
let lines = data.split(/[\r\n]+/);
|
||||
lines.pop();
|
||||
for (let i = 0; i < lines.length; i++)
|
||||
listener.process(lines[i]);
|
||||
}
|
||||
else
|
||||
this.buffer = data;
|
||||
|
||||
if (timeLineID)
|
||||
{
|
||||
TimeLine.asyncEnd(timeLineID);
|
||||
}
|
||||
},
|
||||
onStopRequest: function(request, context, result)
|
||||
{
|
||||
if (timeLineID)
|
||||
{
|
||||
TimeLine.asyncStart(timeLineID);
|
||||
}
|
||||
|
||||
if (Components.isSuccessCode(result) && this.buffer.length)
|
||||
listener.process(this.buffer);
|
||||
listener.process(null);
|
||||
|
||||
if (timeLineID)
|
||||
{
|
||||
TimeLine.asyncEnd(timeLineID);
|
||||
TimeLine.asyncDone(timeLineID);
|
||||
}
|
||||
|
||||
if (!Components.isSuccessCode(result))
|
||||
{
|
||||
let e = Cc["@mozilla.org/js/xpc/Exception;1"].createInstance(Ci.nsIXPCException);
|
||||
e.initialize("File read operation failed", result, null, Components.stack, file, null);
|
||||
callback(e);
|
||||
}
|
||||
else
|
||||
callback(null);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
callback(e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes string data to a file asynchronously, optionally encodes it into
|
||||
* UTF-8 first. The callback will be called when the write operation is done.
|
||||
*/
|
||||
writeToFile: function(/**nsIFile*/ file, /**Boolean*/ encode, /**Iterator*/ data, /**Function*/ callback, /**String*/ timeLineID)
|
||||
{
|
||||
try
|
||||
{
|
||||
let fileStream = FileUtils.openSafeFileOutputStream(file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
|
||||
|
||||
let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
|
||||
pipe.init(true, true, 0, 0x8000, null);
|
||||
|
||||
let outStream = pipe.outputStream;
|
||||
if (encode)
|
||||
{
|
||||
outStream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
|
||||
outStream.init(pipe.outputStream, "UTF-8", 0, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
|
||||
}
|
||||
|
||||
let copier = Cc["@mozilla.org/network/async-stream-copier;1"].createInstance(Ci.nsIAsyncStreamCopier);
|
||||
copier.init(pipe.inputStream, fileStream, null, true, false, 0x8000, true, true);
|
||||
copier.asyncCopy({
|
||||
onStartRequest: function(request, context) {},
|
||||
onStopRequest: function(request, context, result)
|
||||
{
|
||||
if (timeLineID)
|
||||
{
|
||||
TimeLine.asyncDone(timeLineID);
|
||||
}
|
||||
|
||||
if (!Components.isSuccessCode(result))
|
||||
{
|
||||
let e = Cc["@mozilla.org/js/xpc/Exception;1"].createInstance(Ci.nsIXPCException);
|
||||
e.initialize("File write operation failed", result, null, Components.stack, file, null);
|
||||
callback(e);
|
||||
}
|
||||
else
|
||||
callback(null);
|
||||
}
|
||||
}, null);
|
||||
|
||||
let lineBreak = this.lineBreak;
|
||||
function writeNextChunk()
|
||||
{
|
||||
let buf = [];
|
||||
bufLen = 0;
|
||||
while (bufLen < 0x4000)
|
||||
{
|
||||
try
|
||||
{
|
||||
let str = data.next();
|
||||
buf.push(str);
|
||||
bufLen += str.length;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
if (e instanceof StopIteration)
|
||||
break;
|
||||
else if (typeof e == "number")
|
||||
pipe.outputStream.closeWithStatus(e);
|
||||
else if (e instanceof Ci.nsIException)
|
||||
pipe.outputStream.closeWithStatus(e.result);
|
||||
else
|
||||
{
|
||||
Cu.reportError(e);
|
||||
pipe.outputStream.closeWithStatus(Cr.NS_ERROR_FAILURE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pipe.outputStream.asyncWait({
|
||||
onOutputStreamReady: function()
|
||||
{
|
||||
if (timeLineID)
|
||||
{
|
||||
TimeLine.asyncStart(timeLineID);
|
||||
}
|
||||
|
||||
if (buf.length)
|
||||
{
|
||||
let str = buf.join(lineBreak) + lineBreak;
|
||||
if (encode)
|
||||
outStream.writeString(str);
|
||||
else
|
||||
outStream.write(str, str.length);
|
||||
writeNextChunk();
|
||||
}
|
||||
else
|
||||
outStream.close();
|
||||
|
||||
if (timeLineID)
|
||||
{
|
||||
TimeLine.asyncEnd(timeLineID);
|
||||
}
|
||||
}
|
||||
}, 0, 0, Services.tm.currentThread);
|
||||
}
|
||||
writeNextChunk();
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
callback(e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Copies a file asynchronously. The callback will be called when the copy
|
||||
* operation is done.
|
||||
*/
|
||||
copyFile: function(/**nsIFile*/ fromFile, /**nsIFile*/ toFile, /**Function*/ callback)
|
||||
{
|
||||
try
|
||||
{
|
||||
let inStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
|
||||
inStream.init(fromFile, FileUtils.MODE_RDONLY, 0, Ci.nsIFile.DEFER_OPEN);
|
||||
|
||||
let outStream = FileUtils.openFileOutputStream(toFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
|
||||
|
||||
NetUtil.asyncCopy(inStream, outStream, function(result)
|
||||
{
|
||||
if (!Components.isSuccessCode(result))
|
||||
{
|
||||
let e = Cc["@mozilla.org/js/xpc/Exception;1"].createInstance(Ci.nsIXPCException);
|
||||
e.initialize("File write operation failed", result, null, Components.stack, file, null);
|
||||
callback(e);
|
||||
}
|
||||
else
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
callback(e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Renames a file within the same directory, will call callback when done.
|
||||
*/
|
||||
renameFile: function(/**nsIFile*/ fromFile, /**String*/ newName, /**Function*/ callback)
|
||||
{
|
||||
try
|
||||
{
|
||||
fromFile.moveTo(null, newName);
|
||||
callback(null);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
callback(e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a file, will call callback when done.
|
||||
*/
|
||||
removeFile: function(/**nsIFile*/ file, /**Function*/ callback)
|
||||
{
|
||||
try
|
||||
{
|
||||
file.remove(false);
|
||||
callback(null);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
callback(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Matcher class implementing matching addresses against a list of filters.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Matcher", "CombinedMatcher", "defaultMatcher"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "FilterClasses.jsm");
|
||||
|
||||
/**
|
||||
* Blacklist/whitelist filter matching
|
||||
* @constructor
|
||||
*/
|
||||
function Matcher()
|
||||
{
|
||||
this.clear();
|
||||
}
|
||||
|
||||
Matcher.prototype = {
|
||||
/**
|
||||
* Lookup table for filters by their associated keyword
|
||||
* @type Object
|
||||
*/
|
||||
filterByKeyword: null,
|
||||
|
||||
/**
|
||||
* Lookup table for keywords by the filter text
|
||||
* @type Object
|
||||
*/
|
||||
keywordByFilter: null,
|
||||
|
||||
/**
|
||||
* Removes all known filters
|
||||
*/
|
||||
clear: function()
|
||||
{
|
||||
this.filterByKeyword = {__proto__: null};
|
||||
this.keywordByFilter = {__proto__: null};
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a filter to the matcher
|
||||
* @param {RegExpFilter} filter
|
||||
*/
|
||||
add: function(filter)
|
||||
{
|
||||
if (filter.text in this.keywordByFilter)
|
||||
return;
|
||||
|
||||
// Look for a suitable keyword
|
||||
let keyword = this.findKeyword(filter);
|
||||
let oldEntry = this.filterByKeyword[keyword];
|
||||
if (typeof oldEntry == "undefined")
|
||||
this.filterByKeyword[keyword] = filter;
|
||||
else if (oldEntry.length == 1)
|
||||
this.filterByKeyword[keyword] = [oldEntry, filter];
|
||||
else
|
||||
oldEntry.push(filter);
|
||||
this.keywordByFilter[filter.text] = keyword;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a filter from the matcher
|
||||
* @param {RegExpFilter} filter
|
||||
*/
|
||||
remove: function(filter)
|
||||
{
|
||||
if (!(filter.text in this.keywordByFilter))
|
||||
return;
|
||||
|
||||
let keyword = this.keywordByFilter[filter.text];
|
||||
let list = this.filterByKeyword[keyword];
|
||||
if (list.length <= 1)
|
||||
delete this.filterByKeyword[keyword];
|
||||
else
|
||||
{
|
||||
let index = list.indexOf(filter);
|
||||
if (index >= 0)
|
||||
{
|
||||
list.splice(index, 1);
|
||||
if (list.length == 1)
|
||||
this.filterByKeyword[keyword] = list[0];
|
||||
}
|
||||
}
|
||||
|
||||
delete this.keywordByFilter[filter.text];
|
||||
},
|
||||
|
||||
/**
|
||||
* Chooses a keyword to be associated with the filter
|
||||
* @param {String} text text representation of the filter
|
||||
* @return {String} keyword (might be empty string)
|
||||
*/
|
||||
findKeyword: function(filter)
|
||||
{
|
||||
// For donottrack filters use "donottrack" as keyword if nothing else matches
|
||||
let defaultResult = (filter.contentType & RegExpFilter.typeMap.DONOTTRACK ? "donottrack" : "");
|
||||
|
||||
let text = filter.text;
|
||||
if (Filter.regexpRegExp.test(text))
|
||||
return defaultResult;
|
||||
|
||||
// Remove options
|
||||
let match = Filter.optionsRegExp.exec(text);
|
||||
if (match)
|
||||
text = match.input.substr(0, match.index);
|
||||
|
||||
// Remove whitelist marker
|
||||
if (text.substr(0, 2) == "@@")
|
||||
text = text.substr(2);
|
||||
|
||||
let candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g);
|
||||
if (!candidates)
|
||||
return defaultResult;
|
||||
|
||||
let hash = this.filterByKeyword;
|
||||
let result = defaultResult;
|
||||
let resultCount = 0xFFFFFF;
|
||||
let resultLength = 0;
|
||||
for (let i = 0, l = candidates.length; i < l; i++)
|
||||
{
|
||||
let candidate = candidates[i].substr(1);
|
||||
let count = (candidate in hash ? hash[candidate].length : 0);
|
||||
if (count < resultCount || (count == resultCount && candidate.length > resultLength))
|
||||
{
|
||||
result = candidate;
|
||||
resultCount = count;
|
||||
resultLength = candidate.length;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether a particular filter is being matched against.
|
||||
*/
|
||||
hasFilter: function(/**RegExpFilter*/ filter) /**Boolean*/
|
||||
{
|
||||
return (filter.text in this.keywordByFilter);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the keyword used for a filter, null for unknown filters.
|
||||
*/
|
||||
getKeywordForFilter: function(/**RegExpFilter*/ filter) /**String*/
|
||||
{
|
||||
if (filter.text in this.keywordByFilter)
|
||||
return this.keywordByFilter[filter.text];
|
||||
else
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether the entries for a particular keyword match a URL
|
||||
*/
|
||||
_checkEntryMatch: function(keyword, location, contentType, docDomain, thirdParty)
|
||||
{
|
||||
let list = this.filterByKeyword[keyword];
|
||||
for (let i = 0; i < list.length; i++)
|
||||
{
|
||||
let filter = list[i];
|
||||
if (filter.matches(location, contentType, docDomain, thirdParty))
|
||||
return filter;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Tests whether the URL matches any of the known filters
|
||||
* @param {String} location URL to be tested
|
||||
* @param {String} contentType content type identifier of the URL
|
||||
* @param {String} docDomain domain name of the document that loads the URL
|
||||
* @param {Boolean} thirdParty should be true if the URL is a third-party request
|
||||
* @return {RegExpFilter} matching filter or null
|
||||
*/
|
||||
matchesAny: function(location, contentType, docDomain, thirdParty)
|
||||
{
|
||||
let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
|
||||
if (candidates === null)
|
||||
candidates = [];
|
||||
if (contentType == "DONOTTRACK")
|
||||
candidates.unshift("donottrack");
|
||||
else
|
||||
candidates.push("");
|
||||
for (let i = 0, l = candidates.length; i < l; i++)
|
||||
{
|
||||
let substr = candidates[i];
|
||||
if (substr in this.filterByKeyword)
|
||||
{
|
||||
let result = this._checkEntryMatch(substr, location, contentType, docDomain, thirdParty);
|
||||
if (result)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Combines a matcher for blocking and exception rules, automatically sorts
|
||||
* rules into two Matcher instances.
|
||||
* @constructor
|
||||
*/
|
||||
function CombinedMatcher()
|
||||
{
|
||||
this.blacklist = new Matcher();
|
||||
this.whitelist = new Matcher();
|
||||
this.resultCache = {__proto__: null};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximal number of matching cache entries to be kept
|
||||
* @type Number
|
||||
*/
|
||||
CombinedMatcher.maxCacheEntries = 1000;
|
||||
|
||||
CombinedMatcher.prototype =
|
||||
{
|
||||
/**
|
||||
* Matcher for blocking rules.
|
||||
* @type Matcher
|
||||
*/
|
||||
blacklist: null,
|
||||
|
||||
/**
|
||||
* Matcher for exception rules.
|
||||
* @type Matcher
|
||||
*/
|
||||
whitelist: null,
|
||||
|
||||
/**
|
||||
* Lookup table of previous matchesAny results
|
||||
* @type Object
|
||||
*/
|
||||
resultCache: null,
|
||||
|
||||
/**
|
||||
* Number of entries in resultCache
|
||||
* @type Number
|
||||
*/
|
||||
cacheEntries: 0,
|
||||
|
||||
/**
|
||||
* @see Matcher#clear
|
||||
*/
|
||||
clear: function()
|
||||
{
|
||||
this.blacklist.clear();
|
||||
this.whitelist.clear();
|
||||
this.resultCache = {__proto__: null};
|
||||
this.cacheEntries = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* @see Matcher#add
|
||||
*/
|
||||
add: function(filter)
|
||||
{
|
||||
if (filter instanceof WhitelistFilter)
|
||||
{
|
||||
this.whitelist.add(filter);
|
||||
}
|
||||
else
|
||||
this.blacklist.add(filter);
|
||||
|
||||
if (this.cacheEntries > 0)
|
||||
{
|
||||
this.resultCache = {__proto__: null};
|
||||
this.cacheEntries = 0;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @see Matcher#remove
|
||||
*/
|
||||
remove: function(filter)
|
||||
{
|
||||
if (filter instanceof WhitelistFilter)
|
||||
{
|
||||
this.whitelist.remove(filter);
|
||||
}
|
||||
else
|
||||
this.blacklist.remove(filter);
|
||||
|
||||
if (this.cacheEntries > 0)
|
||||
{
|
||||
this.resultCache = {__proto__: null};
|
||||
this.cacheEntries = 0;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @see Matcher#findKeyword
|
||||
*/
|
||||
findKeyword: function(filter)
|
||||
{
|
||||
if (filter instanceof WhitelistFilter)
|
||||
return this.whitelist.findKeyword(filter);
|
||||
else
|
||||
return this.blacklist.findKeyword(filter);
|
||||
},
|
||||
|
||||
/**
|
||||
* @see Matcher#hasFilter
|
||||
*/
|
||||
hasFilter: function(filter)
|
||||
{
|
||||
if (filter instanceof WhitelistFilter)
|
||||
return this.whitelist.hasFilter(filter);
|
||||
else
|
||||
return this.blacklist.hasFilter(filter);
|
||||
},
|
||||
|
||||
/**
|
||||
* @see Matcher#getKeywordForFilter
|
||||
*/
|
||||
getKeywordForFilter: function(filter)
|
||||
{
|
||||
if (filter instanceof WhitelistFilter)
|
||||
return this.whitelist.getKeywordForFilter(filter);
|
||||
else
|
||||
return this.blacklist.getKeywordForFilter(filter);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether a particular filter is slow
|
||||
*/
|
||||
isSlowFilter: function(/**RegExpFilter*/ filter) /**Boolean*/
|
||||
{
|
||||
let matcher = (filter instanceof WhitelistFilter ? this.whitelist : this.blacklist);
|
||||
if (matcher.hasFilter(filter))
|
||||
return !matcher.getKeywordForFilter(filter);
|
||||
else
|
||||
return !matcher.findKeyword(filter);
|
||||
},
|
||||
|
||||
/**
|
||||
* Optimized filter matching testing both whitelist and blacklist matchers
|
||||
* simultaneously. For parameters see Matcher.matchesAny().
|
||||
* @see Matcher#matchesAny
|
||||
*/
|
||||
matchesAnyInternal: function(location, contentType, docDomain, thirdParty)
|
||||
{
|
||||
let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
|
||||
if (candidates === null)
|
||||
candidates = [];
|
||||
if (contentType == "DONOTTRACK")
|
||||
candidates.unshift("donottrack");
|
||||
else
|
||||
candidates.push("");
|
||||
|
||||
let blacklistHit = null;
|
||||
for (let i = 0, l = candidates.length; i < l; i++)
|
||||
{
|
||||
let substr = candidates[i];
|
||||
if (substr in this.whitelist.filterByKeyword)
|
||||
{
|
||||
let result = this.whitelist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty);
|
||||
if (result)
|
||||
return result;
|
||||
}
|
||||
if (substr in this.blacklist.filterByKeyword && blacklistHit === null)
|
||||
blacklistHit = this.blacklist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty);
|
||||
}
|
||||
return blacklistHit;
|
||||
},
|
||||
|
||||
/**
|
||||
* @see Matcher#matchesAny
|
||||
*/
|
||||
matchesAny: function(location, contentType, docDomain, thirdParty)
|
||||
{
|
||||
let key = location + " " + contentType + " " + docDomain + " " + thirdParty;
|
||||
if (key in this.resultCache)
|
||||
return this.resultCache[key];
|
||||
|
||||
let result = this.matchesAnyInternal(location, contentType, docDomain, thirdParty);
|
||||
|
||||
if (this.cacheEntries >= CombinedMatcher.maxCacheEntries)
|
||||
{
|
||||
this.resultCache = {__proto__: null};
|
||||
this.cacheEntries = 0;
|
||||
}
|
||||
|
||||
this.resultCache[key] = result;
|
||||
this.cacheEntries++;
|
||||
|
||||
return result;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared CombinedMatcher instance that should usually be used.
|
||||
* @type CombinedMatcher
|
||||
*/
|
||||
var defaultMatcher = new CombinedMatcher();
|
||||
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileOverview Code responsible for showing and hiding object tabs.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
var EXPORTED_SYMBOLS = ["objectMouseEventHander"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
Cu.import(baseURL + "Prefs.jsm");
|
||||
Cu.import(baseURL + "RequestNotifier.jsm");
|
||||
|
||||
// Run asynchronously to prevent cyclic module loads
|
||||
Utils.runAsync(function()
|
||||
{
|
||||
Cu.import(baseURL + "ContentPolicy.jsm");
|
||||
});
|
||||
|
||||
/**
|
||||
* Class responsible for showing and hiding object tabs.
|
||||
* @class
|
||||
*/
|
||||
var objTabs =
|
||||
{
|
||||
/**
|
||||
* Number of milliseconds to wait until hiding tab after the mouse moves away.
|
||||
* @type Integer
|
||||
*/
|
||||
HIDE_DELAY: 1000,
|
||||
|
||||
/**
|
||||
* Flag used to trigger object tabs initialization first time object tabs are
|
||||
* used.
|
||||
* @type Boolean
|
||||
*/
|
||||
initialized: false,
|
||||
|
||||
/**
|
||||
* Will be set to true while initialization is in progress.
|
||||
* @type Boolean
|
||||
*/
|
||||
initializing: false,
|
||||
|
||||
/**
|
||||
* Parameters for _showTab, to be called once initialization is complete.
|
||||
*/
|
||||
delayedShowParams: null,
|
||||
|
||||
/**
|
||||
* Randomly generated class to be used for visible object tabs on top of object.
|
||||
* @type String
|
||||
*/
|
||||
objTabClassVisibleTop: null,
|
||||
|
||||
/**
|
||||
* Randomly generated class to be used for visible object tabs at the bottom of the object.
|
||||
* @type String
|
||||
*/
|
||||
objTabClassVisibleBottom: null,
|
||||
|
||||
/**
|
||||
* Randomly generated class to be used for invisible object tabs.
|
||||
* @type String
|
||||
*/
|
||||
objTabClassHidden: null,
|
||||
|
||||
/**
|
||||
* Document element the object tab is currently being displayed for.
|
||||
* @type Element
|
||||
*/
|
||||
currentElement: null,
|
||||
|
||||
/**
|
||||
* Windows that the window event handler is currently registered for.
|
||||
* @type Array of Window
|
||||
*/
|
||||
windowListeners: null,
|
||||
|
||||
/**
|
||||
* Panel element currently used as object tab.
|
||||
* @type Element
|
||||
*/
|
||||
objtabElement: null,
|
||||
|
||||
/**
|
||||
* Time of previous position update.
|
||||
* @type Integer
|
||||
*/
|
||||
prevPositionUpdate: 0,
|
||||
|
||||
/**
|
||||
* Timer used to update position of the object tab.
|
||||
* @type nsITimer
|
||||
*/
|
||||
positionTimer: null,
|
||||
|
||||
/**
|
||||
* Timer used to delay hiding of the object tab.
|
||||
* @type nsITimer
|
||||
*/
|
||||
hideTimer: null,
|
||||
|
||||
/**
|
||||
* Used when hideTimer is running, time when the tab should be hidden.
|
||||
* @type Integer
|
||||
*/
|
||||
hideTargetTime: 0,
|
||||
|
||||
/**
|
||||
* Initializes object tabs (generates random classes and registers stylesheet).
|
||||
*/
|
||||
_initCSS: function()
|
||||
{
|
||||
this.delayedShowParams = arguments;
|
||||
|
||||
if (!this.initializing)
|
||||
{
|
||||
this.initializing = true;
|
||||
|
||||
function processCSSData(data)
|
||||
{
|
||||
let rnd = [];
|
||||
let offset = "a".charCodeAt(0);
|
||||
for (let i = 0; i < 60; i++)
|
||||
rnd.push(offset + Math.random() * 26);
|
||||
|
||||
this.objTabClassVisibleTop = String.fromCharCode.apply(String, rnd.slice(0, 20));
|
||||
this.objTabClassVisibleBottom = String.fromCharCode.apply(String, rnd.slice(20, 40));
|
||||
this.objTabClassHidden = String.fromCharCode.apply(String, rnd.slice(40, 60));
|
||||
|
||||
let url = Utils.makeURI("data:text/css," + encodeURIComponent(data.replace(/%%CLASSVISIBLETOP%%/g, this.objTabClassVisibleTop)
|
||||
.replace(/%%CLASSVISIBLEBOTTOM%%/g, this.objTabClassVisibleBottom)
|
||||
.replace(/%%CLASSHIDDEN%%/g, this.objTabClassHidden)));
|
||||
Utils.styleService.loadAndRegisterSheet(url, Ci.nsIStyleSheetService.USER_SHEET);
|
||||
|
||||
this.initializing = false;
|
||||
this.initialized = true;
|
||||
|
||||
if (this.delayedShowParams)
|
||||
this._showTab.apply(this, this.delayedShowParams);
|
||||
}
|
||||
|
||||
// Load CSS asynchronously
|
||||
try {
|
||||
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
|
||||
request.open("GET", "chrome://@ADDON_CHROME_NAME@/content/objtabs.css");
|
||||
request.overrideMimeType("text/plain");
|
||||
|
||||
let me = this;
|
||||
request.addEventListener("load", function()
|
||||
{
|
||||
processCSSData.call(me, request.responseText);
|
||||
}, false);
|
||||
request.send(null);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
this.initializing = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called to show object tab for an element.
|
||||
*/
|
||||
showTabFor: function(/**Element*/ element)
|
||||
{
|
||||
if (!Prefs.frameobjects)
|
||||
return;
|
||||
|
||||
if (this.hideTimer)
|
||||
{
|
||||
this.hideTimer.cancel();
|
||||
this.hideTimer = null;
|
||||
}
|
||||
|
||||
if (this.objtabElement)
|
||||
this.objtabElement.style.setProperty("opacity", "1", "important");
|
||||
|
||||
if (this.currentElement != element)
|
||||
{
|
||||
this._hideTab();
|
||||
|
||||
let data = RequestNotifier.getDataForNode(element, true, Policy.type.OBJECT);
|
||||
if (data)
|
||||
{
|
||||
let hooks = this.getHooksForElement(element);
|
||||
if (hooks)
|
||||
{
|
||||
if (this.initialized)
|
||||
this._showTab(hooks, element, data[1]);
|
||||
else
|
||||
this._initCSS(hooks, element, data[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Looks up the chrome window containing an element and returns abp-hooks
|
||||
* element for this window if any.
|
||||
*/
|
||||
getHooksForElement: function(/**Element*/ element) /**Element*/
|
||||
{
|
||||
let doc = element.ownerDocument.defaultView
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow)
|
||||
.document;
|
||||
let hooks = doc.getElementById("abp-hooks");
|
||||
if (hooks && hooks.wrappedJSObject)
|
||||
hooks = hooks.wrappedJSObject;
|
||||
return hooks;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called to hide object tab for an element (actual hiding happens delayed).
|
||||
*/
|
||||
hideTabFor: function(/**Element*/ element)
|
||||
{
|
||||
if (element != this.currentElement || this.hideTimer)
|
||||
return;
|
||||
|
||||
this.hideTargetTime = Date.now() + this.HIDE_DELAY;
|
||||
this.hideTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
this.hideTimer.init(this, 40, Ci.nsITimer.TYPE_REPEATING_SLACK);
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes the tab element visible.
|
||||
*/
|
||||
_showTab: function(/**Element*/ hooks, /**Element*/ element, /**RequestEntry*/ data)
|
||||
{
|
||||
let doc = element.ownerDocument.defaultView.top.document;
|
||||
|
||||
this.objtabElement = doc.createElementNS("http://www.w3.org/1999/xhtml", "a");
|
||||
this.objtabElement.textContent = hooks.getAttribute("objtabtext");
|
||||
this.objtabElement.setAttribute("title", hooks.getAttribute("objtabtooltip"));
|
||||
this.objtabElement.setAttribute("href", data.location);
|
||||
this.objtabElement.setAttribute("class", this.objTabClassHidden);
|
||||
this.objtabElement.style.setProperty("opacity", "1", "important");
|
||||
this.objtabElement.nodeData = data;
|
||||
this.objtabElement.hooks = hooks;
|
||||
|
||||
this.currentElement = element;
|
||||
|
||||
// Register paint listeners for the relevant windows
|
||||
this.windowListeners = [];
|
||||
let wnd = element.ownerDocument.defaultView;
|
||||
while (wnd)
|
||||
{
|
||||
wnd.addEventListener("MozAfterPaint", objectWindowEventHandler, false);
|
||||
this.windowListeners.push(wnd);
|
||||
wnd = (wnd.parent != wnd ? wnd.parent : null);
|
||||
}
|
||||
|
||||
// Register mouse listeners on the object tab
|
||||
this.objtabElement.addEventListener("mouseover", objectTabEventHander, false);
|
||||
this.objtabElement.addEventListener("mouseout", objectTabEventHander, false);
|
||||
this.objtabElement.addEventListener("click", objectTabEventHander, true);
|
||||
|
||||
// Insert the tab into the document and adjust its position
|
||||
doc.documentElement.appendChild(this.objtabElement);
|
||||
if (!this.positionTimer)
|
||||
{
|
||||
this.positionTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
this.positionTimer.init(this, 200, Ci.nsITimer.TYPE_REPEATING_SLACK);
|
||||
}
|
||||
this._positionTab();
|
||||
},
|
||||
|
||||
/**
|
||||
* Hides the tab element.
|
||||
*/
|
||||
_hideTab: function()
|
||||
{
|
||||
this.delayedShowParams = null;
|
||||
|
||||
if (this.objtabElement)
|
||||
{
|
||||
// Prevent recursive calls via popuphidden handler
|
||||
let objtab = this.objtabElement;
|
||||
this.objtabElement = null;
|
||||
this.currentElement = null;
|
||||
|
||||
if (this.hideTimer)
|
||||
{
|
||||
this.hideTimer.cancel();
|
||||
this.hideTimer = null;
|
||||
}
|
||||
|
||||
if (this.positionTimer)
|
||||
{
|
||||
this.positionTimer.cancel();
|
||||
this.positionTimer = null;
|
||||
}
|
||||
|
||||
try {
|
||||
objtab.parentNode.removeChild(objtab);
|
||||
} catch (e) {}
|
||||
objtab.removeEventListener("mouseover", objectTabEventHander, false);
|
||||
objtab.removeEventListener("mouseout", objectTabEventHander, false);
|
||||
objtab.nodeData = null;
|
||||
|
||||
for each (let wnd in this.windowListeners)
|
||||
wnd.removeEventListener("MozAfterPaint", objectWindowEventHandler, false);
|
||||
this.windowListeners = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates position of the tab element.
|
||||
*/
|
||||
_positionTab: function()
|
||||
{
|
||||
// Test whether element is still in document
|
||||
let elementDoc = null;
|
||||
try
|
||||
{
|
||||
elementDoc = this.currentElement.ownerDocument;
|
||||
} catch (e) {} // Ignore "can't access dead object" error
|
||||
if (!elementDoc || !this.currentElement.offsetWidth || !this.currentElement.offsetHeight ||
|
||||
!elementDoc.defaultView || !elementDoc.documentElement)
|
||||
{
|
||||
this._hideTab();
|
||||
return;
|
||||
}
|
||||
|
||||
let objRect = this._getElementPosition(this.currentElement);
|
||||
|
||||
let className = this.objTabClassVisibleTop;
|
||||
let left = objRect.right - this.objtabElement.offsetWidth;
|
||||
let top = objRect.top - this.objtabElement.offsetHeight;
|
||||
if (top < 0)
|
||||
{
|
||||
top = objRect.bottom;
|
||||
className = this.objTabClassVisibleBottom;
|
||||
}
|
||||
|
||||
if (this.objtabElement.style.left != left + "px")
|
||||
this.objtabElement.style.setProperty("left", left + "px", "important");
|
||||
if (this.objtabElement.style.top != top + "px")
|
||||
this.objtabElement.style.setProperty("top", top + "px", "important");
|
||||
|
||||
if (this.objtabElement.getAttribute("class") != className)
|
||||
this.objtabElement.setAttribute("class", className);
|
||||
|
||||
this.prevPositionUpdate = Date.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates element's position relative to the top frame and considering
|
||||
* clipping due to scrolling.
|
||||
* @return {left: Number, top: Number, right: Number, bottom: Number}
|
||||
*/
|
||||
_getElementPosition: function(/**Element*/ element)
|
||||
{
|
||||
// Restrict rectangle coordinates by the boundaries of a window's client area
|
||||
function intersectRect(rect, wnd)
|
||||
{
|
||||
// Cannot use wnd.innerWidth/Height because they won't account for scrollbars
|
||||
let doc = wnd.document;
|
||||
let wndWidth = doc.documentElement.clientWidth;
|
||||
let wndHeight = doc.documentElement.clientHeight;
|
||||
if (doc.compatMode == "BackCompat") // clientHeight will be bogus in quirks mode
|
||||
wndHeight = Math.max(doc.documentElement.offsetHeight, doc.body.offsetHeight) - wnd.scrollMaxY - 1;
|
||||
|
||||
rect.left = Math.max(rect.left, 0);
|
||||
rect.top = Math.max(rect.top, 0);
|
||||
rect.right = Math.min(rect.right, wndWidth);
|
||||
rect.bottom = Math.min(rect.bottom, wndHeight);
|
||||
}
|
||||
|
||||
let rect = element.getBoundingClientRect();
|
||||
let wnd = element.ownerDocument.defaultView;
|
||||
|
||||
let style = wnd.getComputedStyle(element, null);
|
||||
let offsets = [
|
||||
parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft),
|
||||
parseFloat(style.borderTopWidth) + parseFloat(style.paddingTop),
|
||||
parseFloat(style.borderRightWidth) + parseFloat(style.paddingRight),
|
||||
parseFloat(style.borderBottomWidth) + parseFloat(style.paddingBottom)
|
||||
];
|
||||
|
||||
rect = {left: rect.left + offsets[0], top: rect.top + offsets[1],
|
||||
right: rect.right - offsets[2], bottom: rect.bottom - offsets[3]};
|
||||
while (true)
|
||||
{
|
||||
intersectRect(rect, wnd);
|
||||
|
||||
if (!wnd.frameElement)
|
||||
break;
|
||||
|
||||
// Recalculate coordinates to be relative to frame's parent window
|
||||
let frameElement = wnd.frameElement;
|
||||
wnd = frameElement.ownerDocument.defaultView;
|
||||
|
||||
let frameRect = frameElement.getBoundingClientRect();
|
||||
let frameStyle = wnd.getComputedStyle(frameElement, null);
|
||||
let relLeft = frameRect.left + parseFloat(frameStyle.borderLeftWidth) + parseFloat(frameStyle.paddingLeft);
|
||||
let relTop = frameRect.top + parseFloat(frameStyle.borderTopWidth) + parseFloat(frameStyle.paddingTop);
|
||||
|
||||
rect.left += relLeft;
|
||||
rect.right += relLeft;
|
||||
rect.top += relTop;
|
||||
rect.bottom += relTop;
|
||||
}
|
||||
|
||||
return rect;
|
||||
},
|
||||
|
||||
doBlock: function()
|
||||
{
|
||||
Cu.import(baseURL + "AppIntegration.jsm");
|
||||
let wrapper = AppIntegration.getWrapperForWindow(this.objtabElement.hooks.ownerDocument.defaultView);
|
||||
if (wrapper)
|
||||
wrapper.blockItem(this.currentElement, this.objtabElement.nodeData);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called whenever a timer fires.
|
||||
*/
|
||||
observe: function(/**nsISupport*/ subject, /**String*/ topic, /**String*/ data)
|
||||
{
|
||||
if (subject == this.positionTimer)
|
||||
{
|
||||
// Don't update position if it was already updated recently (via MozAfterPaint)
|
||||
if (Date.now() - this.prevPositionUpdate > 100)
|
||||
this._positionTab();
|
||||
}
|
||||
else if (subject == this.hideTimer)
|
||||
{
|
||||
let now = Date.now();
|
||||
if (now >= this.hideTargetTime)
|
||||
this._hideTab();
|
||||
else if (this.hideTargetTime - now < this.HIDE_DELAY / 2)
|
||||
this.objtabElement.style.setProperty("opacity", (this.hideTargetTime - now) * 2 / this.HIDE_DELAY, "important");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function called whenever the mouse enters or leaves an object.
|
||||
*/
|
||||
function objectMouseEventHander(/**Event*/ event)
|
||||
{
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
|
||||
if (event.type == "mouseover")
|
||||
objTabs.showTabFor(event.target);
|
||||
else if (event.type == "mouseout")
|
||||
objTabs.hideTabFor(event.target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called for paint events of the object tab window.
|
||||
*/
|
||||
function objectWindowEventHandler(/**Event*/ event)
|
||||
{
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
|
||||
// Don't trigger update too often, avoid overusing CPU on frequent page updates
|
||||
if (event.type == "MozAfterPaint" && Date.now() - objTabs.prevPositionUpdate > 20)
|
||||
objTabs._positionTab();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called whenever the mouse enters or leaves an object tab.
|
||||
*/
|
||||
function objectTabEventHander(/**Event*/ event)
|
||||
{
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
|
||||
if (event.type == "click" && event.button == 0)
|
||||
{
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
objTabs.doBlock();
|
||||
}
|
||||
else if (event.type == "mouseover")
|
||||
objTabs.showTabFor(objTabs.currentElement);
|
||||
else if (event.type == "mouseout")
|
||||
objTabs.hideTabFor(objTabs.currentElement);
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Manages Adblock Plus preferences.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Prefs"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import(baseURL + "TimeLine.jsm");
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
|
||||
const prefRoot = "extensions.@ADDON_CHROME_NAME@.";
|
||||
|
||||
/**
|
||||
* Will be set to true if Adblock Plus is scheduled to be uninstalled on
|
||||
* browser restart.
|
||||
* @type Boolean
|
||||
*/
|
||||
let willBeUninstalled = false;
|
||||
|
||||
/**
|
||||
* Preferences branch containing Adblock Plus preferences.
|
||||
* @type nsIPrefBranch
|
||||
*/
|
||||
let branch = Utils.prefService.getBranch(prefRoot);
|
||||
|
||||
/**
|
||||
* List of listeners to be notified whenever preferences are updated
|
||||
* @type Array of Function
|
||||
*/
|
||||
let listeners = [];
|
||||
|
||||
/**
|
||||
* This object allows easy access to Adblock Plus preferences, all defined
|
||||
* preferences will be available as its members.
|
||||
* @class
|
||||
*/
|
||||
var Prefs =
|
||||
{
|
||||
/**
|
||||
* Will be set to true if the user enters private browsing mode.
|
||||
* @type Boolean
|
||||
*/
|
||||
privateBrowsing: false,
|
||||
|
||||
/**
|
||||
* Called on module startup.
|
||||
*/
|
||||
startup: function()
|
||||
{
|
||||
TimeLine.enter("Entered Prefs.startup()");
|
||||
|
||||
// Initialize prefs list
|
||||
let defaultBranch = this.defaultBranch;
|
||||
for each (let name in defaultBranch.getChildList("", {}))
|
||||
{
|
||||
let type = defaultBranch.getPrefType(name);
|
||||
switch (type)
|
||||
{
|
||||
case Ci.nsIPrefBranch.PREF_INT:
|
||||
defineIntegerProperty(name);
|
||||
break;
|
||||
case Ci.nsIPrefBranch.PREF_BOOL:
|
||||
defineBooleanProperty(name);
|
||||
break;
|
||||
case Ci.nsIPrefBranch.PREF_STRING:
|
||||
defineStringProperty(name);
|
||||
break;
|
||||
}
|
||||
if ("_update_" + name in PrefsPrivate)
|
||||
PrefsPrivate["_update_" + name]();
|
||||
}
|
||||
|
||||
// Always disable object tabs in Fennec, they aren't usable
|
||||
if (Utils.isFennec)
|
||||
Prefs.frameobjects = false;
|
||||
|
||||
TimeLine.log("done loading initial values");
|
||||
|
||||
// Register observers
|
||||
TimeLine.log("registering observers");
|
||||
registerObservers();
|
||||
|
||||
TimeLine.leave("Prefs.startup() done");
|
||||
},
|
||||
|
||||
/**
|
||||
* Backwards compatibility, this pref is optional
|
||||
*/
|
||||
get patternsfile() /**String*/
|
||||
{
|
||||
let result = null;
|
||||
try
|
||||
{
|
||||
result = branch.getCharPref("patternsfile");
|
||||
} catch(e) {}
|
||||
this.__defineGetter__("patternsfile", function() result);
|
||||
return this.patternsfile;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the preferences branch containing default preference values.
|
||||
*/
|
||||
get defaultBranch() /**nsIPreferenceBranch*/
|
||||
{
|
||||
return Utils.prefService.getDefaultBranch(prefRoot);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called on module shutdown.
|
||||
*/
|
||||
shutdown: function()
|
||||
{
|
||||
TimeLine.enter("Entered Prefs.shutdown()");
|
||||
|
||||
if (willBeUninstalled)
|
||||
{
|
||||
// Make sure that a new installation after uninstall will be treated like
|
||||
// an update.
|
||||
try {
|
||||
branch.clearUserPref("currentVersion");
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
TimeLine.leave("Prefs.shutdown() done");
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a preferences listener that will be fired whenever preferences are
|
||||
* reloaded
|
||||
*/
|
||||
addListener: function(/**Function*/ listener)
|
||||
{
|
||||
let index = listeners.indexOf(listener);
|
||||
if (index < 0)
|
||||
listeners.push(listener);
|
||||
},
|
||||
/**
|
||||
* Removes a preferences listener
|
||||
*/
|
||||
removeListener: function(/**Function*/ listener)
|
||||
{
|
||||
let index = listeners.indexOf(listener);
|
||||
if (index >= 0)
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Private nsIObserver implementation
|
||||
* @class
|
||||
*/
|
||||
var PrefsPrivate =
|
||||
{
|
||||
/**
|
||||
* If set to true notifications about preference changes will no longer cause
|
||||
* a reload. This is to prevent unnecessary reloads while saving.
|
||||
* @type Boolean
|
||||
*/
|
||||
ignorePrefChanges: false,
|
||||
|
||||
/**
|
||||
* nsIObserver implementation
|
||||
*/
|
||||
observe: function(subject, topic, data)
|
||||
{
|
||||
if (topic == "private-browsing")
|
||||
{
|
||||
if (data == "enter")
|
||||
Prefs.privateBrowsing = true;
|
||||
else if (data == "exit")
|
||||
Prefs.privateBrowsing = false;
|
||||
}
|
||||
else if (topic == "em-action-requested")
|
||||
{
|
||||
if (subject instanceof Ci.nsIUpdateItem && subject.id == Utils.addonID)
|
||||
willBeUninstalled = (data == "item-uninstalled");
|
||||
}
|
||||
else if (topic == "nsPref:changed" && !this.ignorePrefChanges && "_update_" + data in PrefsPrivate)
|
||||
PrefsPrivate["_update_" + data]();
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver])
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds observers to keep various properties of Prefs object updated.
|
||||
*/
|
||||
function registerObservers()
|
||||
{
|
||||
// Observe preferences changes
|
||||
try {
|
||||
branch.QueryInterface(Ci.nsIPrefBranchInternal)
|
||||
.addObserver("", PrefsPrivate, true);
|
||||
}
|
||||
catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
|
||||
Services.obs.addObserver(PrefsPrivate, "em-action-requested", true);
|
||||
|
||||
// Add Private Browsing observer
|
||||
if ("@mozilla.org/privatebrowsing;1" in Cc)
|
||||
{
|
||||
try
|
||||
{
|
||||
Prefs.privateBrowsing = Cc["@mozilla.org/privatebrowsing;1"].getService(Ci.nsIPrivateBrowsingService).privateBrowsingEnabled;
|
||||
Services.obs.addObserver(PrefsPrivate, "private-browsing", true);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers preference listeners whenever a preference is changed.
|
||||
*/
|
||||
function triggerListeners(/**String*/ name)
|
||||
{
|
||||
for each (let listener in listeners)
|
||||
listener(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up getter/setter on Prefs object for preference.
|
||||
*/
|
||||
function defineProperty(/**String*/ name, defaultValue, /**Function*/ readFunc, /**Function*/ writeFunc)
|
||||
{
|
||||
let value = defaultValue;
|
||||
PrefsPrivate["_update_" + name] = function()
|
||||
{
|
||||
try
|
||||
{
|
||||
value = readFunc();
|
||||
triggerListeners(name);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
Prefs.__defineGetter__(name, function() value);
|
||||
Prefs.__defineSetter__(name, function(newValue)
|
||||
{
|
||||
if (value == newValue)
|
||||
return value;
|
||||
|
||||
try
|
||||
{
|
||||
PrefsPrivate.ignorePrefChanges = true;
|
||||
writeFunc(newValue);
|
||||
value = newValue;
|
||||
triggerListeners(name);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrefsPrivate.ignorePrefChanges = false;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up getter/setter on Prefs object for an integer preference.
|
||||
*/
|
||||
function defineIntegerProperty(/**String*/ name)
|
||||
{
|
||||
defineProperty(name, 0, function() branch.getIntPref(name),
|
||||
function(newValue) branch.setIntPref(name, newValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up getter/setter on Prefs object for a boolean preference.
|
||||
*/
|
||||
function defineBooleanProperty(/**String*/ name)
|
||||
{
|
||||
defineProperty(name, false, function() branch.getBoolPref(name),
|
||||
function(newValue) branch.setBoolPref(name, newValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up getter/setter on Prefs object for a string preference.
|
||||
*/
|
||||
function defineStringProperty(/**String*/ name)
|
||||
{
|
||||
defineProperty(name, "", function() branch.getComplexValue(name, Ci.nsISupportsString).data,
|
||||
function(newValue)
|
||||
{
|
||||
let str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
|
||||
str.data = newValue;
|
||||
branch.setComplexValue(name, Ci.nsISupportsString, str);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Public Adblock Plus API.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["AdblockPlus"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
Cu.import(baseURL + "FilterStorage.jsm");
|
||||
Cu.import(baseURL + "FilterClasses.jsm");
|
||||
Cu.import(baseURL + "SubscriptionClasses.jsm");
|
||||
|
||||
const externalPrefix = "~external~";
|
||||
|
||||
/**
|
||||
* Class implementing public Adblock Plus API
|
||||
* @class
|
||||
*/
|
||||
var AdblockPlus =
|
||||
{
|
||||
/**
|
||||
* Returns current subscription count
|
||||
* @type Integer
|
||||
*/
|
||||
get subscriptionCount()
|
||||
{
|
||||
return FilterStorage.subscriptions.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a subscription by its URL
|
||||
*/
|
||||
getSubscription: function(/**String*/ id) /**IAdblockPlusSubscription*/
|
||||
{
|
||||
if (id in FilterStorage.knownSubscriptions)
|
||||
return createSubscriptionWrapper(FilterStorage.knownSubscriptions[id]);
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a subscription by its position in the list
|
||||
*/
|
||||
getSubscriptionAt: function(/**Integer*/ index) /**IAdblockPlusSubscription*/
|
||||
{
|
||||
if (index < 0 || index >= FilterStorage.subscriptions.length)
|
||||
return null;
|
||||
|
||||
return createSubscriptionWrapper(FilterStorage.subscriptions[index]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates an external subscription and creates it if necessary
|
||||
*/
|
||||
updateExternalSubscription: function(/**String*/ id, /**String*/ title, /**Array of Filter*/ filters) /**String*/
|
||||
{
|
||||
if (id.substr(0, externalPrefix.length) != externalPrefix)
|
||||
id = externalPrefix + id;
|
||||
let subscription = Subscription.fromURL(id);
|
||||
if (!subscription)
|
||||
subscription = new ExternalSubscription(id, title);
|
||||
|
||||
subscription.lastDownload = parseInt(new Date().getTime() / 1000);
|
||||
|
||||
let newFilters = [];
|
||||
for each (let filter in filters)
|
||||
{
|
||||
filter = Filter.fromText(Filter.normalize(filter));
|
||||
if (filter)
|
||||
newFilters.push(filter);
|
||||
}
|
||||
|
||||
if (id in FilterStorage.knownSubscriptions)
|
||||
FilterStorage.updateSubscriptionFilters(subscription, newFilters);
|
||||
else
|
||||
{
|
||||
subscription.filters = newFilters;
|
||||
FilterStorage.addSubscription(subscription);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes an external subscription by its identifier
|
||||
*/
|
||||
removeExternalSubscription: function(/**String*/ id) /**Boolean*/
|
||||
{
|
||||
if (id.substr(0, externalPrefix.length) != externalPrefix)
|
||||
id = externalPrefix + id;
|
||||
if (!(id in FilterStorage.knownSubscriptions))
|
||||
return false;
|
||||
|
||||
FilterStorage.removeSubscription(FilterStorage.knownSubscriptions[id]);
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds user-defined filters to the list
|
||||
*/
|
||||
addPatterns: function(/**Array of String*/ filters)
|
||||
{
|
||||
for each (let filter in filters)
|
||||
{
|
||||
filter = Filter.fromText(Filter.normalize(filter));
|
||||
if (filter)
|
||||
{
|
||||
filter.disabled = false;
|
||||
FilterStorage.addFilter(filter);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes user-defined filters from the list
|
||||
*/
|
||||
removePatterns: function(/**Array of String*/ filters)
|
||||
{
|
||||
for each (let filter in filters)
|
||||
{
|
||||
filter = Filter.fromText(Filter.normalize(filter));
|
||||
if (filter)
|
||||
FilterStorage.removeFilter(filter);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns installed Adblock Plus version
|
||||
*/
|
||||
getInstalledVersion: function() /**String*/
|
||||
{
|
||||
return Utils.addonVersion;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns source code revision this Adblock Plus build was created from (if available)
|
||||
*/
|
||||
getInstalledBuild: function() /**String*/
|
||||
{
|
||||
return Utils.addonBuild;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps a subscription into IAdblockPlusSubscription structure.
|
||||
*/
|
||||
function createSubscriptionWrapper(/**Subscription*/ subscription) /**IAdblockPlusSubscription*/
|
||||
{
|
||||
if (!subscription)
|
||||
return null;
|
||||
|
||||
return {
|
||||
url: subscription.url,
|
||||
special: subscription instanceof SpecialSubscription,
|
||||
title: subscription.title,
|
||||
autoDownload: true,
|
||||
disabled: subscription.disabled,
|
||||
external: subscription instanceof ExternalSubscription,
|
||||
lastDownload: subscription instanceof RegularSubscription ? subscription.lastDownload : 0,
|
||||
downloadStatus: subscription instanceof DownloadableSubscription ? subscription.downloadStatus : "synchronize_ok",
|
||||
lastModified: subscription instanceof DownloadableSubscription ? subscription.lastModified : null,
|
||||
expires: subscription instanceof DownloadableSubscription ? subscription.expires : 0,
|
||||
getPatterns: function()
|
||||
{
|
||||
let result = subscription.filters.map(function(filter)
|
||||
{
|
||||
return filter.text;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Stores Adblock Plus data to be attached to a window.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["RequestNotifier"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
Cu.import(baseURL + "FilterClasses.jsm");
|
||||
Utils.runAsync(Cu.import, Cu, baseURL + "ContentPolicy.jsm"); // delay to avoid circular imports
|
||||
|
||||
let nodeData = new WeakMap();
|
||||
let windowStats = new WeakMap();
|
||||
let windowSelection = new WeakMap();
|
||||
|
||||
/**
|
||||
* List of notifiers in use - these notifiers need to receive notifications on
|
||||
* new requests.
|
||||
* @type RequestNotifier[]
|
||||
*/
|
||||
let activeNotifiers = [];
|
||||
|
||||
/**
|
||||
* Creates a notifier object for a particular window. After creation the window
|
||||
* will first be scanned for previously saved requests. Once that scan is
|
||||
* complete only new requests for this window will be reported.
|
||||
* @param {Window} wnd window to attach the notifier to
|
||||
* @param {Function} listener listener to be called whenever a new request is found
|
||||
* @param {Object} [listenerObj] "this" pointer to be used when calling the listener
|
||||
*/
|
||||
function RequestNotifier(wnd, listener, listenerObj)
|
||||
{
|
||||
this.window = wnd;
|
||||
this.listener = listener;
|
||||
this.listenerObj = listenerObj || null;
|
||||
activeNotifiers.push(this);
|
||||
if (wnd)
|
||||
this.startScan(wnd);
|
||||
else
|
||||
this.scanComplete = true;
|
||||
}
|
||||
RequestNotifier.prototype =
|
||||
{
|
||||
/**
|
||||
* The window this notifier is associated with.
|
||||
* @type Window
|
||||
*/
|
||||
window: null,
|
||||
|
||||
/**
|
||||
* The listener to be called when a new request is found.
|
||||
* @type Function
|
||||
*/
|
||||
listener: null,
|
||||
|
||||
/**
|
||||
* "this" pointer to be used when calling the listener.
|
||||
* @type Object
|
||||
*/
|
||||
listenerObj: null,
|
||||
|
||||
/**
|
||||
* Will be set to true once the initial window scan is complete.
|
||||
* @type Boolean
|
||||
*/
|
||||
scanComplete: false,
|
||||
|
||||
/**
|
||||
* Shuts down the notifier once it is no longer used. The listener
|
||||
* will no longer be called after that.
|
||||
*/
|
||||
shutdown: function()
|
||||
{
|
||||
delete this.window;
|
||||
delete this.listener;
|
||||
delete this.listenerObj;
|
||||
|
||||
for (let i = activeNotifiers.length - 1; i >= 0; i--)
|
||||
if (activeNotifiers[i] == this)
|
||||
activeNotifiers.splice(i, 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies listener about a new request.
|
||||
*/
|
||||
notifyListener: function(/**Window*/ wnd, /**Node*/ node, /**RequestEntry*/ entry)
|
||||
{
|
||||
this.listener.call(this.listenerObj, wnd, node, entry, this.scanComplete);
|
||||
},
|
||||
|
||||
/**
|
||||
* Number of currently posted scan events (will be 0 when the scan finishes
|
||||
* running).
|
||||
*/
|
||||
eventsPosted: 0,
|
||||
|
||||
/**
|
||||
* Starts the initial scan of the window (will recurse into frames).
|
||||
* @param {Window} wnd the window to be scanned
|
||||
*/
|
||||
startScan: function(wnd)
|
||||
{
|
||||
let currentThread = Utils.threadManager.currentThread;
|
||||
|
||||
let doc = wnd.document;
|
||||
let walker = doc.createTreeWalker(doc, Ci.nsIDOMNodeFilter.SHOW_ELEMENT, null, false);
|
||||
|
||||
let runnable =
|
||||
{
|
||||
notifier: null,
|
||||
|
||||
run: function()
|
||||
{
|
||||
if (!this.notifier.listener)
|
||||
return;
|
||||
|
||||
let node = walker.currentNode;
|
||||
let data = nodeData.get(node);
|
||||
if (typeof data != "undefined")
|
||||
for (let i = data.length - 1; i >= 0; i--)
|
||||
this.notifier.notifyListener(wnd, node, data[i]);
|
||||
|
||||
if (walker.nextNode())
|
||||
currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
|
||||
else
|
||||
{
|
||||
// Done with the current window, start the scan for its frames
|
||||
for (let i = 0; i < wnd.frames.length; i++)
|
||||
this.notifier.startScan(wnd.frames[i]);
|
||||
|
||||
this.notifier.eventsPosted--;
|
||||
if (!this.notifier.eventsPosted)
|
||||
{
|
||||
this.notifier.scanComplete = true;
|
||||
this.notifier.notifyListener(wnd, null, null);
|
||||
}
|
||||
|
||||
this.notifier = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
runnable.notifier = this;
|
||||
|
||||
// Process each node in a separate event on current thread to allow other
|
||||
// events to process
|
||||
this.eventsPosted++;
|
||||
currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
|
||||
}
|
||||
};
|
||||
|
||||
RequestNotifier.storeSelection = function(/**Window*/ wnd, /**String*/ selection)
|
||||
{
|
||||
windowSelection.set(wnd, selection);
|
||||
};
|
||||
RequestNotifier.getSelection = function(/**Window*/ wnd) /**String*/
|
||||
{
|
||||
if (windowSelection.has(wnd))
|
||||
return windowSelection.get(wnd);
|
||||
else
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches request data to a DOM node.
|
||||
* @param {Node} node node to attach data to
|
||||
* @param {Window} topWnd top-level window the node belongs to
|
||||
* @param {Integer} contentType request type, one of the Policy.type.* constants
|
||||
* @param {String} docDomain domain of the document that initiated the request
|
||||
* @param {Boolean} thirdParty will be true if a third-party server has been requested
|
||||
* @param {String} location the address that has been requested
|
||||
* @param {Filter} filter filter applied to the request or null if none
|
||||
*/
|
||||
RequestNotifier.addNodeData = function(/**Node*/ node, /**Window*/ topWnd, /**Integer*/ contentType, /**String*/ docDomain, /**Boolean*/ thirdParty, /**String*/ location, /**Filter*/ filter)
|
||||
{
|
||||
return new RequestEntry(node, topWnd, contentType, docDomain, thirdParty, location, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the statistics for a window.
|
||||
* @result {Object} Object with the properties items, blocked, whitelisted, hidden, filters containing statistics for the window (might be null)
|
||||
*/
|
||||
RequestNotifier.getWindowStatistics = function(/**Window*/ wnd)
|
||||
{
|
||||
if (windowStats.has(wnd.document))
|
||||
return windowStats.get(wnd.document);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the request entry associated with a DOM node.
|
||||
* @param {Node} node
|
||||
* @param {Boolean} noParent if missing or false, the search will extend to the parent nodes until one is found that has data associated with it
|
||||
* @param {Integer} [type] request type to be looking for
|
||||
* @param {String} [location] request location to be looking for
|
||||
* @result {[Node, RequestEntry]}
|
||||
* @static
|
||||
*/
|
||||
RequestNotifier.getDataForNode = function(node, noParent, type, location)
|
||||
{
|
||||
while (node)
|
||||
{
|
||||
let data = nodeData.get(node);
|
||||
if (typeof data != "undefined")
|
||||
{
|
||||
// Look for matching entry starting at the end of the list (most recent first)
|
||||
for (let i = data.length - 1; i >= 0; i--)
|
||||
{
|
||||
let entry = data[i];
|
||||
if ((typeof type == "undefined" || entry.type == type) &&
|
||||
(typeof location == "undefined" || entry.location == location))
|
||||
{
|
||||
return [node, entry];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have any match on this node then maybe its parent will do
|
||||
if ((typeof noParent != "boolean" || !noParent) &&
|
||||
node.parentNode instanceof Ci.nsIDOMElement)
|
||||
{
|
||||
node = node.parentNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
node = null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function RequestEntry(node, topWnd, contentType, docDomain, thirdParty, location, filter)
|
||||
{
|
||||
this.type = contentType;
|
||||
this.docDomain = docDomain;
|
||||
this.thirdParty = thirdParty;
|
||||
this.location = location;
|
||||
this.filter = filter;
|
||||
|
||||
this.attachToNode(node);
|
||||
|
||||
// Update window statistics
|
||||
if (!windowStats.has(topWnd.document))
|
||||
{
|
||||
windowStats.set(topWnd.document, {
|
||||
items: 0,
|
||||
hidden: 0,
|
||||
blocked: 0,
|
||||
whitelisted: 0,
|
||||
filters: {}
|
||||
});
|
||||
}
|
||||
|
||||
let stats = windowStats.get(topWnd.document);
|
||||
if (filter && filter instanceof ElemHideFilter)
|
||||
stats.hidden++;
|
||||
else
|
||||
stats.items++;
|
||||
if (filter)
|
||||
{
|
||||
if (filter instanceof BlockingFilter)
|
||||
stats.blocked++;
|
||||
else if (filter instanceof WhitelistFilter)
|
||||
stats.whitelisted++;
|
||||
|
||||
if (filter.text in stats.filters)
|
||||
stats.filters[filter.text]++;
|
||||
else
|
||||
stats.filters[filter.text] = 1;
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
for each (let notifier in activeNotifiers)
|
||||
if (!notifier.window || notifier.window == topWnd)
|
||||
notifier.notifyListener(topWnd, node, this);
|
||||
}
|
||||
RequestEntry.prototype =
|
||||
{
|
||||
/**
|
||||
* Content type of the request (one of the nsIContentPolicy constants)
|
||||
* @type Integer
|
||||
*/
|
||||
type: null,
|
||||
/**
|
||||
* Domain name of the requesting document
|
||||
* @type String
|
||||
*/
|
||||
docDomain: null,
|
||||
/**
|
||||
* True if the request goes to a different domain than the domain of the containing document
|
||||
* @type Boolean
|
||||
*/
|
||||
thirdParty: false,
|
||||
/**
|
||||
* Address being requested
|
||||
* @type String
|
||||
*/
|
||||
location: null,
|
||||
/**
|
||||
* Filter that was applied to this request (if any)
|
||||
* @type Filter
|
||||
*/
|
||||
filter: null,
|
||||
/**
|
||||
* String representation of the content type, e.g. "subdocument"
|
||||
* @type String
|
||||
*/
|
||||
get typeDescr() Policy.typeDescr[this.type],
|
||||
/**
|
||||
* User-visible localized representation of the content type, e.g. "frame"
|
||||
* @type String
|
||||
*/
|
||||
get localizedDescr() Policy.localizedDescr[this.type],
|
||||
|
||||
/**
|
||||
* Attaches this request object to a DOM node.
|
||||
*/
|
||||
attachToNode: function(/**Node*/ node)
|
||||
{
|
||||
let existingData = nodeData.get(node);
|
||||
if (typeof existingData != "undefined")
|
||||
{
|
||||
// Add the new entry to the existing data
|
||||
existingData.push(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Associate the node with a new array
|
||||
nodeData.set(node, [this]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,560 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Definition of Subscription class and its subclasses.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Subscription", "SpecialSubscription", "RegularSubscription", "ExternalSubscription", "DownloadableSubscription"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
Cu.import(baseURL + "FilterClasses.jsm");
|
||||
Cu.import(baseURL + "FilterNotifier.jsm");
|
||||
|
||||
/**
|
||||
* Abstract base class for filter subscriptions
|
||||
*
|
||||
* @param {String} url download location of the subscription
|
||||
* @param {String} [title] title of the filter subscription
|
||||
* @constructor
|
||||
*/
|
||||
function Subscription(url, title)
|
||||
{
|
||||
this.url = url;
|
||||
this.filters = [];
|
||||
this._title = title || Utils.getString("newGroup_title");
|
||||
Subscription.knownSubscriptions[url] = this;
|
||||
}
|
||||
Subscription.prototype =
|
||||
{
|
||||
/**
|
||||
* Download location of the subscription
|
||||
* @type String
|
||||
*/
|
||||
url: null,
|
||||
|
||||
/**
|
||||
* Filters contained in the filter subscription
|
||||
* @type Array of Filter
|
||||
*/
|
||||
filters: null,
|
||||
|
||||
_title: null,
|
||||
_fixedTitle: false,
|
||||
_disabled: false,
|
||||
|
||||
/**
|
||||
* Title of the filter subscription
|
||||
* @type String
|
||||
*/
|
||||
get title() this._title,
|
||||
set title(value)
|
||||
{
|
||||
if (value != this._title)
|
||||
{
|
||||
let oldValue = this._title;
|
||||
this._title = value;
|
||||
FilterNotifier.triggerListeners("subscription.title", this, value, oldValue);
|
||||
}
|
||||
return this._title;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines whether the title should be editable
|
||||
* @type Boolean
|
||||
*/
|
||||
get fixedTitle() this._fixedTitle,
|
||||
set fixedTitle(value)
|
||||
{
|
||||
if (value != this._fixedTitle)
|
||||
{
|
||||
let oldValue = this._fixedTitle;
|
||||
this._fixedTitle = value;
|
||||
FilterNotifier.triggerListeners("subscription.fixedTitle", this, value, oldValue);
|
||||
}
|
||||
return this._fixedTitle;
|
||||
},
|
||||
|
||||
/**
|
||||
* Defines whether the filters in the subscription should be disabled
|
||||
* @type Boolean
|
||||
*/
|
||||
get disabled() this._disabled,
|
||||
set disabled(value)
|
||||
{
|
||||
if (value != this._disabled)
|
||||
{
|
||||
let oldValue = this._disabled;
|
||||
this._disabled = value;
|
||||
FilterNotifier.triggerListeners("subscription.disabled", this, value, oldValue);
|
||||
}
|
||||
return this._disabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Serializes the filter to an array of strings for writing out on the disk.
|
||||
* @param {Array of String} buffer buffer to push the serialization results into
|
||||
*/
|
||||
serialize: function(buffer)
|
||||
{
|
||||
buffer.push("[Subscription]");
|
||||
buffer.push("url=" + this.url);
|
||||
buffer.push("title=" + this._title);
|
||||
if (this._fixedTitle)
|
||||
buffer.push("fixedTitle=true");
|
||||
if (this._disabled)
|
||||
buffer.push("disabled=true");
|
||||
},
|
||||
|
||||
serializeFilters: function(buffer)
|
||||
{
|
||||
for each (let filter in this.filters)
|
||||
buffer.push(filter.text.replace(/\[/g, "\\["));
|
||||
},
|
||||
|
||||
toString: function()
|
||||
{
|
||||
let buffer = [];
|
||||
this.serialize(buffer);
|
||||
return buffer.join("\n");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache for known filter subscriptions, maps URL to subscription objects.
|
||||
* @type Object
|
||||
*/
|
||||
Subscription.knownSubscriptions = {__proto__: null};
|
||||
|
||||
/**
|
||||
* Returns a subscription from its URL, creates a new one if necessary.
|
||||
* @param {String} url URL of the subscription
|
||||
* @return {Subscription} subscription or null if the subscription couldn't be created
|
||||
*/
|
||||
Subscription.fromURL = function(url)
|
||||
{
|
||||
if (url in Subscription.knownSubscriptions)
|
||||
return Subscription.knownSubscriptions[url];
|
||||
|
||||
try
|
||||
{
|
||||
// Test URL for validity
|
||||
url = Utils.ioService.newURI(url, null, null).spec;
|
||||
return new DownloadableSubscription(url, null);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
return new SpecialSubscription(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a subscription
|
||||
*
|
||||
* @param {Object} obj map of serialized properties and their values
|
||||
* @return {Subscription} subscription or null if the subscription couldn't be created
|
||||
*/
|
||||
Subscription.fromObject = function(obj)
|
||||
{
|
||||
let result;
|
||||
try
|
||||
{
|
||||
obj.url = Utils.ioService.newURI(obj.url, null, null).spec;
|
||||
|
||||
// URL is valid - this is a downloadable subscription
|
||||
result = new DownloadableSubscription(obj.url, obj.title);
|
||||
if ("nextURL" in obj)
|
||||
result.nextURL = obj.nextURL;
|
||||
if ("downloadStatus" in obj)
|
||||
result._downloadStatus = obj.downloadStatus;
|
||||
if ("lastModified" in obj)
|
||||
result.lastModified = obj.lastModified;
|
||||
if ("lastSuccess" in obj)
|
||||
result.lastSuccess = parseInt(obj.lastSuccess) || 0;
|
||||
if ("lastCheck" in obj)
|
||||
result._lastCheck = parseInt(obj.lastCheck) || 0;
|
||||
if ("expires" in obj)
|
||||
result.expires = parseInt(obj.expires) || 0;
|
||||
if ("softExpiration" in obj)
|
||||
result.softExpiration = parseInt(obj.softExpiration) || 0;
|
||||
if ("errors" in obj)
|
||||
result._errors = parseInt(obj.errors) || 0;
|
||||
if ("requiredVersion" in obj)
|
||||
{
|
||||
result.requiredVersion = obj.requiredVersion;
|
||||
if (Utils.versionComparator.compare(result.requiredVersion, "3.5.0") > 0)
|
||||
result.upgradeRequired = true;
|
||||
}
|
||||
if ("alternativeLocations" in obj)
|
||||
result.alternativeLocations = obj.alternativeLocations;
|
||||
if ("homepage" in obj)
|
||||
result._homepage = obj.homepage;
|
||||
if ("lastDownload" in obj)
|
||||
result._lastDownload = parseInt(obj.lastDownload) || 0;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Invalid URL - custom filter group
|
||||
if (!("title" in obj))
|
||||
{
|
||||
// Backwards compatibility - titles and filter types were originally
|
||||
// determined by group identifier.
|
||||
if (obj.url == "~wl~")
|
||||
obj.defaults = "whitelist";
|
||||
else if (obj.url == "~fl~")
|
||||
obj.defaults = "blocking";
|
||||
else if (obj.url == "~eh~")
|
||||
obj.defaults = "elemhide";
|
||||
if ("defaults" in obj)
|
||||
obj.title = Utils.getString(obj.defaults + "Group_title");
|
||||
}
|
||||
result = new SpecialSubscription(obj.url, obj.title);
|
||||
if ("defaults" in obj)
|
||||
result.defaults = obj.defaults.split(" ");
|
||||
}
|
||||
if ("fixedTitle" in obj)
|
||||
result._fixedTitle = (obj.fixedTitle == "true");
|
||||
if ("disabled" in obj)
|
||||
result._disabled = (obj.disabled == "true");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for special filter subscriptions (user's filters)
|
||||
* @param {String} url see Subscription()
|
||||
* @param {String} [title] see Subscription()
|
||||
* @constructor
|
||||
* @augments Subscription
|
||||
*/
|
||||
function SpecialSubscription(url, title)
|
||||
{
|
||||
Subscription.call(this, url, title);
|
||||
}
|
||||
SpecialSubscription.prototype =
|
||||
{
|
||||
__proto__: Subscription.prototype,
|
||||
|
||||
/**
|
||||
* Filter types that should be added to this subscription by default
|
||||
* (entries should correspond to keys in SpecialSubscription.defaultsMap).
|
||||
* @type Array of String
|
||||
*/
|
||||
defaults: null,
|
||||
|
||||
/**
|
||||
* Tests whether a filter should be added to this group by default
|
||||
* @param {Filter} filter filter to be tested
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isDefaultFor: function(filter)
|
||||
{
|
||||
if (this.defaults && this.defaults.length)
|
||||
{
|
||||
for each (let type in this.defaults)
|
||||
{
|
||||
if (filter instanceof SpecialSubscription.defaultsMap[type])
|
||||
return true;
|
||||
if (!(filter instanceof ActiveFilter) && type == "blacklist")
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* See Subscription.serialize()
|
||||
*/
|
||||
serialize: function(buffer)
|
||||
{
|
||||
Subscription.prototype.serialize.call(this, buffer);
|
||||
if (this.defaults && this.defaults.length)
|
||||
buffer.push("defaults=" + this.defaults.filter(function(type) type in SpecialSubscription.defaultsMap).join(" "));
|
||||
if (this._lastDownload)
|
||||
buffer.push("lastDownload=" + this._lastDownload);
|
||||
}
|
||||
};
|
||||
|
||||
SpecialSubscription.defaultsMap = {
|
||||
__proto__: null,
|
||||
"whitelist": WhitelistFilter,
|
||||
"blocking": BlockingFilter,
|
||||
"elemhide": ElemHideFilter
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new user-defined filter group.
|
||||
* @param {String} [title] title of the new filter group
|
||||
* @result {SpecialSubscription}
|
||||
*/
|
||||
SpecialSubscription.create = function(title)
|
||||
{
|
||||
let url;
|
||||
do
|
||||
{
|
||||
url = "~user~" + Math.round(Math.random()*1000000);
|
||||
} while (url in Subscription.knownSubscriptions);
|
||||
return new SpecialSubscription(url, title)
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new user-defined filter group and adds the given filter to it.
|
||||
* This group will act as the default group for this filter type.
|
||||
*/
|
||||
SpecialSubscription.createForFilter = function(/**Filter*/ filter) /**SpecialSubscription*/
|
||||
{
|
||||
let subscription = SpecialSubscription.create();
|
||||
subscription.filters.push(filter);
|
||||
for (let type in SpecialSubscription.defaultsMap)
|
||||
{
|
||||
if (filter instanceof SpecialSubscription.defaultsMap[type])
|
||||
subscription.defaults = [type];
|
||||
}
|
||||
if (!subscription.defaults)
|
||||
subscription.defaults = ["blocking"];
|
||||
subscription.title = Utils.getString(subscription.defaults[0] + "Group_title");
|
||||
return subscription;
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstract base class for regular filter subscriptions (both internally and externally updated)
|
||||
* @param {String} url see Subscription()
|
||||
* @param {String} [title] see Subscription()
|
||||
* @constructor
|
||||
* @augments Subscription
|
||||
*/
|
||||
function RegularSubscription(url, title)
|
||||
{
|
||||
Subscription.call(this, url, title || url);
|
||||
}
|
||||
RegularSubscription.prototype =
|
||||
{
|
||||
__proto__: Subscription.prototype,
|
||||
|
||||
_homepage: null,
|
||||
_lastDownload: 0,
|
||||
|
||||
/**
|
||||
* Filter subscription homepage if known
|
||||
* @type String
|
||||
*/
|
||||
get homepage() this._homepage,
|
||||
set homepage(value)
|
||||
{
|
||||
if (value != this._homepage)
|
||||
{
|
||||
let oldValue = this._homepage;
|
||||
this._homepage = value;
|
||||
FilterNotifier.triggerListeners("subscription.homepage", this, value, oldValue);
|
||||
}
|
||||
return this._homepage;
|
||||
},
|
||||
|
||||
/**
|
||||
* Time of the last subscription download (in seconds since the beginning of the epoch)
|
||||
* @type Number
|
||||
*/
|
||||
get lastDownload() this._lastDownload,
|
||||
set lastDownload(value)
|
||||
{
|
||||
if (value != this._lastDownload)
|
||||
{
|
||||
let oldValue = this._lastDownload;
|
||||
this._lastDownload = value;
|
||||
FilterNotifier.triggerListeners("subscription.lastDownload", this, value, oldValue);
|
||||
}
|
||||
return this._lastDownload;
|
||||
},
|
||||
|
||||
/**
|
||||
* See Subscription.serialize()
|
||||
*/
|
||||
serialize: function(buffer)
|
||||
{
|
||||
Subscription.prototype.serialize.call(this, buffer);
|
||||
if (this._homepage)
|
||||
buffer.push("homepage=" + this._homepage);
|
||||
if (this._lastDownload)
|
||||
buffer.push("lastDownload=" + this._lastDownload);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Class for filter subscriptions updated by externally (by other extension)
|
||||
* @param {String} url see Subscription()
|
||||
* @param {String} [title] see Subscription()
|
||||
* @constructor
|
||||
* @augments RegularSubscription
|
||||
*/
|
||||
function ExternalSubscription(url, title)
|
||||
{
|
||||
RegularSubscription.call(this, url, title);
|
||||
}
|
||||
ExternalSubscription.prototype =
|
||||
{
|
||||
__proto__: RegularSubscription.prototype,
|
||||
|
||||
/**
|
||||
* See Subscription.serialize()
|
||||
*/
|
||||
serialize: function(buffer)
|
||||
{
|
||||
throw new Error("Unexpected call, external subscriptions should not be serialized");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Class for filter subscriptions updated by externally (by other extension)
|
||||
* @param {String} url see Subscription()
|
||||
* @param {String} [title] see Subscription()
|
||||
* @constructor
|
||||
* @augments RegularSubscription
|
||||
*/
|
||||
function DownloadableSubscription(url, title)
|
||||
{
|
||||
RegularSubscription.call(this, url, title);
|
||||
}
|
||||
DownloadableSubscription.prototype =
|
||||
{
|
||||
__proto__: RegularSubscription.prototype,
|
||||
|
||||
_downloadStatus: null,
|
||||
_lastCheck: 0,
|
||||
_errors: 0,
|
||||
|
||||
/**
|
||||
* Next URL the downloaded should be attempted from (in case of redirects)
|
||||
* @type String
|
||||
*/
|
||||
nextURL: null,
|
||||
|
||||
/**
|
||||
* Status of the last download (ID of a string)
|
||||
* @type String
|
||||
*/
|
||||
get downloadStatus() this._downloadStatus,
|
||||
set downloadStatus(value)
|
||||
{
|
||||
let oldValue = this._downloadStatus;
|
||||
this._downloadStatus = value;
|
||||
FilterNotifier.triggerListeners("subscription.downloadStatus", this, value, oldValue);
|
||||
return this._downloadStatus;
|
||||
},
|
||||
|
||||
/**
|
||||
* Value of the Last-Modified header returned by the server on last download
|
||||
* @type String
|
||||
*/
|
||||
lastModified: null,
|
||||
|
||||
/**
|
||||
* Time of the last successful download (in seconds since the beginning of the
|
||||
* epoch).
|
||||
*/
|
||||
lastSuccess: 0,
|
||||
|
||||
/**
|
||||
* Time when the subscription was considered for an update last time (in seconds
|
||||
* since the beginning of the epoch). This will be used to increase softExpiration
|
||||
* if the user doesn't use Adblock Plus for some time.
|
||||
* @type Number
|
||||
*/
|
||||
get lastCheck() this._lastCheck,
|
||||
set lastCheck(value)
|
||||
{
|
||||
if (value != this._lastCheck)
|
||||
{
|
||||
let oldValue = this._lastCheck;
|
||||
this._lastCheck = value;
|
||||
FilterNotifier.triggerListeners("subscription.lastCheck", this, value, oldValue);
|
||||
}
|
||||
return this._lastCheck;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hard expiration time of the filter subscription (in seconds since the beginning of the epoch)
|
||||
* @type Number
|
||||
*/
|
||||
expires: 0,
|
||||
|
||||
/**
|
||||
* Soft expiration time of the filter subscription (in seconds since the beginning of the epoch)
|
||||
* @type Number
|
||||
*/
|
||||
softExpiration: 0,
|
||||
|
||||
/**
|
||||
* Number of download failures since last success
|
||||
* @type Number
|
||||
*/
|
||||
get errors() this._errors,
|
||||
set errors(value)
|
||||
{
|
||||
if (value != this._errors)
|
||||
{
|
||||
let oldValue = this._errors;
|
||||
this._errors = value;
|
||||
FilterNotifier.triggerListeners("subscription.errors", this, value, oldValue);
|
||||
}
|
||||
return this._errors;
|
||||
},
|
||||
|
||||
/**
|
||||
* Minimal Adblock Plus version required for this subscription
|
||||
* @type String
|
||||
*/
|
||||
requiredVersion: null,
|
||||
|
||||
/**
|
||||
* Should be true if requiredVersion is higher than current Adblock Plus version
|
||||
* @type Boolean
|
||||
*/
|
||||
upgradeRequired: false,
|
||||
|
||||
/**
|
||||
* Value of the X-Alternative-Locations header: comma-separated list of URLs
|
||||
* with their weighting factors, e.g.: http://foo.example.com/;q=0.5,http://bar.example.com/;q=2
|
||||
* @type String
|
||||
*/
|
||||
alternativeLocations: null,
|
||||
|
||||
/**
|
||||
* See Subscription.serialize()
|
||||
*/
|
||||
serialize: function(buffer)
|
||||
{
|
||||
RegularSubscription.prototype.serialize.call(this, buffer);
|
||||
if (this.nextURL)
|
||||
buffer.push("nextURL=" + this.nextURL);
|
||||
if (this.downloadStatus)
|
||||
buffer.push("downloadStatus=" + this.downloadStatus);
|
||||
if (this.lastModified)
|
||||
buffer.push("lastModified=" + this.lastModified);
|
||||
if (this.lastSuccess)
|
||||
buffer.push("lastSuccess=" + this.lastSuccess);
|
||||
if (this.lastCheck)
|
||||
buffer.push("lastCheck=" + this.lastCheck);
|
||||
if (this.expires)
|
||||
buffer.push("expires=" + this.expires);
|
||||
if (this.softExpiration)
|
||||
buffer.push("softExpiration=" + this.softExpiration);
|
||||
if (this.errors)
|
||||
buffer.push("errors=" + this.errors);
|
||||
if (this.requiredVersion)
|
||||
buffer.push("requiredVersion=" + this.requiredVersion);
|
||||
if (this.alternativeLocations)
|
||||
buffer.push("alternativeLocations=" + this.alternativeLocations);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Module containing a bunch of utility functions.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Sync"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
Cu.import(baseURL + "FilterStorage.jsm");
|
||||
Cu.import(baseURL + "SubscriptionClasses.jsm");
|
||||
Cu.import(baseURL + "FilterClasses.jsm");
|
||||
Cu.import(baseURL + "FilterNotifier.jsm");
|
||||
Cu.import(baseURL + "Synchronizer.jsm");
|
||||
|
||||
/**
|
||||
* ID of the only record stored
|
||||
* @type String
|
||||
*/
|
||||
const filtersRecordID = "6fad6286-8207-46b6-aa39-8e0ce0bd7c49";
|
||||
|
||||
/**
|
||||
* Weave tracker class (is set when Weave is initialized).
|
||||
*/
|
||||
var Tracker = null;
|
||||
|
||||
var Sync =
|
||||
{
|
||||
/**
|
||||
* Will be set to true if/when Weave starts up.
|
||||
* @type Boolean
|
||||
*/
|
||||
initialized: false,
|
||||
|
||||
/**
|
||||
* Whether Weave requested us to track changes.
|
||||
* @type Boolean
|
||||
*/
|
||||
trackingEnabled: false,
|
||||
|
||||
/**
|
||||
* Called on module startup.
|
||||
*/
|
||||
startup: function()
|
||||
{
|
||||
Services.obs.addObserver(SyncPrivate, "weave:service:ready", true);
|
||||
Services.obs.addObserver(SyncPrivate, "weave:engine:start-tracking", true);
|
||||
Services.obs.addObserver(SyncPrivate, "weave:engine:stop-tracking", true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns Adblock Plus sync engine.
|
||||
* @result Engine
|
||||
*/
|
||||
getEngine: function()
|
||||
{
|
||||
if (this.initialized)
|
||||
return Weave.Engines.get("adblockplus");
|
||||
else
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
var SyncPrivate =
|
||||
{
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
|
||||
|
||||
observe: function(subject, topic, data)
|
||||
{
|
||||
switch (topic)
|
||||
{
|
||||
case "weave:service:ready":
|
||||
Cu.import("resource://services-sync/main.js");
|
||||
|
||||
Tracker = Weave.SyncEngine.prototype._trackerObj;
|
||||
ABPEngine.prototype.__proto__ = Weave.SyncEngine.prototype;
|
||||
ABPStore.prototype.__proto__ = Weave.Store.prototype;
|
||||
ABPTracker.prototype.__proto__ = Tracker.prototype;
|
||||
|
||||
Weave.Engines.register(ABPEngine);
|
||||
Sync.initialized = true;
|
||||
break;
|
||||
case "weave:engine:start-tracking":
|
||||
Sync.trackingEnabled = true;
|
||||
if (trackerInstance)
|
||||
trackerInstance.startTracking();
|
||||
break;
|
||||
case "weave:engine:stop-tracking":
|
||||
Sync.trackingEnabled = false;
|
||||
if (trackerInstance)
|
||||
trackerInstance.stopTracking();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function ABPEngine()
|
||||
{
|
||||
Weave.SyncEngine.call(this, "AdblockPlus");
|
||||
}
|
||||
ABPEngine.prototype =
|
||||
{
|
||||
_storeObj: ABPStore,
|
||||
_trackerObj: ABPTracker,
|
||||
version: 1,
|
||||
|
||||
_reconcile: function(item)
|
||||
{
|
||||
// Always process server data, we will do the merging ourselves
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
function ABPStore(name)
|
||||
{
|
||||
Weave.Store.call(this, name);
|
||||
}
|
||||
ABPStore.prototype =
|
||||
{
|
||||
getAllIDs: function()
|
||||
{
|
||||
let result = {}
|
||||
result[filtersRecordID] = true;
|
||||
return result;
|
||||
},
|
||||
|
||||
changeItemID: function(oldId, newId)
|
||||
{
|
||||
// This should not be called, our engine doesn't implement _findDupe
|
||||
throw Cr.NS_ERROR_UNEXPECTED;
|
||||
},
|
||||
|
||||
itemExists: function(id)
|
||||
{
|
||||
// Only one id exists so far
|
||||
return (id == filtersRecordID);
|
||||
},
|
||||
|
||||
createRecord: function(id, collection)
|
||||
{
|
||||
let record = new ABPEngine.prototype._recordObj(collection, id);
|
||||
if (id == filtersRecordID)
|
||||
{
|
||||
record.cleartext = {
|
||||
id: id,
|
||||
subscriptions: [],
|
||||
};
|
||||
for each (let subscription in FilterStorage.subscriptions)
|
||||
{
|
||||
if (subscription instanceof ExternalSubscription)
|
||||
continue;
|
||||
|
||||
let subscriptionEntry =
|
||||
{
|
||||
url: subscription.url,
|
||||
disabled: subscription.disabled
|
||||
};
|
||||
if (subscription instanceof SpecialSubscription)
|
||||
{
|
||||
subscriptionEntry.filters = [];
|
||||
for each (let filter in subscription.filters)
|
||||
{
|
||||
let filterEntry = {text: filter.text};
|
||||
if (filter instanceof ActiveFilter)
|
||||
filterEntry.disabled = filter.disabled;
|
||||
subscriptionEntry.filters.push(filterEntry);
|
||||
}
|
||||
}
|
||||
else
|
||||
subscriptionEntry.title = subscription.title;
|
||||
record.cleartext.subscriptions.push(subscriptionEntry);
|
||||
}
|
||||
|
||||
// Data sent, forget about local changes now
|
||||
trackerInstance.clearPrivateChanges()
|
||||
}
|
||||
else
|
||||
record.deleted = true;
|
||||
|
||||
return record;
|
||||
},
|
||||
|
||||
create: function(record)
|
||||
{
|
||||
// This should not be called because our record list doesn't change but
|
||||
// call update just in case.
|
||||
this.update(record);
|
||||
},
|
||||
|
||||
update: function(record)
|
||||
{
|
||||
if (record.id != filtersRecordID)
|
||||
return;
|
||||
|
||||
this._log.trace("Merging in remote data");
|
||||
|
||||
let data = record.cleartext.subscriptions;
|
||||
|
||||
// First make sure we have the same subscriptions on both sides
|
||||
let seenSubscription = {__proto__: null};
|
||||
for each (let remoteSubscription in data)
|
||||
{
|
||||
seenSubscription[remoteSubscription.url] = true;
|
||||
if (remoteSubscription.url in FilterStorage.knownSubscriptions)
|
||||
{
|
||||
let subscription = FilterStorage.knownSubscriptions[remoteSubscription.url];
|
||||
if (!trackerInstance.didSubscriptionChange(remoteSubscription))
|
||||
{
|
||||
// Only change local subscription if there were no changes, otherwise dismiss remote changes
|
||||
subscription.disabled = remoteSubscription.disabled;
|
||||
if (subscription instanceof DownloadableSubscription)
|
||||
subscription.title = remoteSubscription.title;
|
||||
}
|
||||
}
|
||||
else if (!trackerInstance.didSubscriptionChange(remoteSubscription))
|
||||
{
|
||||
// Subscription was added remotely, add it locally as well
|
||||
let subscription = Subscription.fromURL(remoteSubscription.url);
|
||||
if (!subscription)
|
||||
continue;
|
||||
|
||||
subscription.disabled = remoteSubscription.disabled;
|
||||
if (subscription instanceof DownloadableSubscription)
|
||||
{
|
||||
subscription.title = remoteSubscription.title;
|
||||
FilterStorage.addSubscription(subscription);
|
||||
Synchronizer.execute(subscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for each (let subscription in FilterStorage.subscriptions.slice())
|
||||
{
|
||||
if (!(subscription.url in seenSubscription) && subscription instanceof DownloadableSubscription && !trackerInstance.didSubscriptionChange(subscription))
|
||||
{
|
||||
// Subscription was removed remotely, remove it locally as well
|
||||
FilterStorage.removeSubscription(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
// Now sync the custom filters
|
||||
let seenFilter = {__proto__: null};
|
||||
for each (let remoteSubscription in data)
|
||||
{
|
||||
if (!("filters" in remoteSubscription))
|
||||
continue;
|
||||
|
||||
for each (let remoteFilter in remoteSubscription.filters)
|
||||
{
|
||||
seenFilter[remoteFilter.text] = true;
|
||||
|
||||
let filter = Filter.fromText(remoteFilter.text);
|
||||
if (!filter || trackerInstance.didFilterChange(filter))
|
||||
continue;
|
||||
|
||||
if (filter.subscriptions.some(function(subscription) subscription instanceof SpecialSubscription))
|
||||
{
|
||||
// Filter might have been changed remotely
|
||||
if (filter instanceof ActiveFilter)
|
||||
filter.disabled = remoteFilter.disabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Filter was added remotely, add it locally as well
|
||||
FilterStorage.addFilter(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for each (let subscription in FilterStorage.subscriptions)
|
||||
{
|
||||
if (!(subscription instanceof SpecialSubscription))
|
||||
continue;
|
||||
|
||||
for each (let filter in subscription.filters.slice())
|
||||
{
|
||||
if (!(filter.text in seenFilter) && !trackerInstance.didFilterChange(filter))
|
||||
{
|
||||
// Filter was removed remotely, remove it locally as well
|
||||
FilterStorage.removeFilter(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge done, forget about local changes now
|
||||
trackerInstance.clearPrivateChanges()
|
||||
},
|
||||
|
||||
remove: function(record)
|
||||
{
|
||||
// Shouldn't be called but if it is - ignore
|
||||
},
|
||||
|
||||
wipe: function()
|
||||
{
|
||||
this._log.trace("Got wipe command, removing all data");
|
||||
|
||||
for each (let subscription in FilterStorage.subscriptions.slice())
|
||||
{
|
||||
if (subscription instanceof DownloadableSubscription)
|
||||
FilterStorage.removeSubscription(subscription);
|
||||
else if (subscription instanceof SpecialSubscription)
|
||||
{
|
||||
for each (let filter in subscription.filters.slice())
|
||||
FilterStorage.removeFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
// Data wiped, forget about local changes now
|
||||
trackerInstance.clearPrivateChanges()
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hack to allow store to use the tracker - store tracker pointer globally.
|
||||
*/
|
||||
let trackerInstance = null;
|
||||
|
||||
function ABPTracker(name)
|
||||
{
|
||||
Tracker.call(this, name);
|
||||
|
||||
this.privateTracker = new Tracker(name + ".private");
|
||||
trackerInstance = this;
|
||||
|
||||
this.onChange = this._bindMethod(this.onChange);
|
||||
|
||||
if (Sync.trackingEnabled)
|
||||
this.startTracking();
|
||||
}
|
||||
ABPTracker.prototype =
|
||||
{
|
||||
privateTracker: null,
|
||||
|
||||
_bindMethod: function(method)
|
||||
{
|
||||
let me = this;
|
||||
return function() method.apply(me, arguments);
|
||||
},
|
||||
|
||||
startTracking: function()
|
||||
{
|
||||
FilterNotifier.addListener(this.onChange);
|
||||
},
|
||||
|
||||
stopTracking: function()
|
||||
{
|
||||
FilterNotifier.removeListener(this.onChange);
|
||||
},
|
||||
|
||||
clearPrivateChanges: function()
|
||||
{
|
||||
this.privateTracker.clearChangedIDs();
|
||||
},
|
||||
|
||||
addPrivateChange: function(id)
|
||||
{
|
||||
// Ignore changes during syncing
|
||||
if (this.ignoreAll)
|
||||
return;
|
||||
|
||||
this.addChangedID(filtersRecordID);
|
||||
this.privateTracker.addChangedID(id);
|
||||
this.score += 10;
|
||||
},
|
||||
|
||||
didSubscriptionChange: function(subscription)
|
||||
{
|
||||
return ("subscription " + subscription.url) in this.privateTracker.changedIDs;
|
||||
},
|
||||
|
||||
didFilterChange: function(filter)
|
||||
{
|
||||
return ("filter " + filter.text) in this.privateTracker.changedIDs;
|
||||
},
|
||||
|
||||
onChange: function(action, item)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "subscription.updated":
|
||||
if ("oldSubscription" in item)
|
||||
{
|
||||
// Subscription moved to a new address
|
||||
this.addPrivateChange("subscription " + item.url);
|
||||
this.addPrivateChange("subscription " + item.oldSubscription.url);
|
||||
}
|
||||
else if (item instanceof SpecialSubscription)
|
||||
{
|
||||
// User's filters changed via Preferences window
|
||||
for each (let filter in item.filters)
|
||||
this.addPrivateChange("filter " + filter.text);
|
||||
for each (let filter in item.oldFilters)
|
||||
this.addPrivateChange("filter " + filter.text);
|
||||
}
|
||||
break;
|
||||
case "subscription.added":
|
||||
case "subscription.removed":
|
||||
case "subscription.disabled":
|
||||
case "subscription.title":
|
||||
this.addPrivateChange("subscription " + item.url);
|
||||
break;
|
||||
case "filter.added":
|
||||
case "filter.removed":
|
||||
case "filter.disabled":
|
||||
this.addPrivateChange("filter " + item.text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,594 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Manages synchronization of filter subscriptions.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Synchronizer"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import(baseURL + "TimeLine.jsm");
|
||||
Cu.import(baseURL + "Utils.jsm");
|
||||
Cu.import(baseURL + "FilterStorage.jsm");
|
||||
Cu.import(baseURL + "FilterNotifier.jsm");
|
||||
Cu.import(baseURL + "FilterClasses.jsm");
|
||||
Cu.import(baseURL + "SubscriptionClasses.jsm");
|
||||
Cu.import(baseURL + "Prefs.jsm");
|
||||
|
||||
const MILLISECONDS_IN_SECOND = 1000;
|
||||
const SECONDS_IN_MINUTE = 60;
|
||||
const SECONDS_IN_HOUR = 60 * SECONDS_IN_MINUTE;
|
||||
const SECONDS_IN_DAY = 24 * SECONDS_IN_HOUR;
|
||||
const INITIAL_DELAY = 6 * SECONDS_IN_MINUTE;
|
||||
const CHECK_INTERVAL = SECONDS_IN_HOUR;
|
||||
const MIN_EXPIRATION_INTERVAL = 1 * SECONDS_IN_DAY;
|
||||
const MAX_EXPIRATION_INTERVAL = 14 * SECONDS_IN_DAY;
|
||||
const MAX_ABSENSE_INTERVAL = 1 * SECONDS_IN_DAY;
|
||||
|
||||
let timer = null;
|
||||
|
||||
/**
|
||||
* Map of subscriptions currently being downloaded, all currently downloaded
|
||||
* URLs are keys of that map.
|
||||
*/
|
||||
let executing = {__proto__: null};
|
||||
|
||||
/**
|
||||
* This object is responsible for downloading filter subscriptions whenever
|
||||
* necessary.
|
||||
* @class
|
||||
*/
|
||||
var Synchronizer =
|
||||
{
|
||||
/**
|
||||
* Called on module startup.
|
||||
*/
|
||||
startup: function()
|
||||
{
|
||||
TimeLine.enter("Entered Synchronizer.startup()");
|
||||
|
||||
let callback = function()
|
||||
{
|
||||
timer.delay = CHECK_INTERVAL * MILLISECONDS_IN_SECOND;
|
||||
checkSubscriptions();
|
||||
};
|
||||
|
||||
timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
timer.initWithCallback(callback, INITIAL_DELAY * MILLISECONDS_IN_SECOND, Ci.nsITimer.TYPE_REPEATING_SLACK);
|
||||
|
||||
TimeLine.leave("Synchronizer.startup() done");
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether a subscription is currently being downloaded.
|
||||
* @param {String} url URL of the subscription
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isExecuting: function(url)
|
||||
{
|
||||
return url in executing;
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts the download of a subscription.
|
||||
* @param {DownloadableSubscription} subscription Subscription to be downloaded
|
||||
* @param {Boolean} manual true for a manually started download (should not trigger fallback requests)
|
||||
* @param {Boolean} forceDownload if true, the subscription will even be redownloaded if it didn't change on the server
|
||||
*/
|
||||
execute: function(subscription, manual, forceDownload)
|
||||
{
|
||||
// Delay execution, SeaMonkey 2.1 won't fire request's event handlers
|
||||
// otherwise if the window that called us is closed.
|
||||
Utils.runAsync(this.executeInternal, this, subscription, manual, forceDownload);
|
||||
},
|
||||
|
||||
executeInternal: function(subscription, manual, forceDownload)
|
||||
{
|
||||
let url = subscription.url;
|
||||
if (url in executing)
|
||||
return;
|
||||
|
||||
let newURL = subscription.nextURL;
|
||||
let hadTemporaryRedirect = false;
|
||||
subscription.nextURL = null;
|
||||
|
||||
let curVersion = "3.5.0";
|
||||
let loadFrom = newURL;
|
||||
let isBaseLocation = true;
|
||||
if (!loadFrom)
|
||||
loadFrom = url;
|
||||
if (loadFrom == url)
|
||||
{
|
||||
if (subscription.alternativeLocations)
|
||||
{
|
||||
// We have alternative download locations, choose one. "Regular"
|
||||
// subscription URL always goes in with weight 1.
|
||||
let options = [[1, url]];
|
||||
let totalWeight = 1;
|
||||
for each (let alternative in subscription.alternativeLocations.split(','))
|
||||
{
|
||||
if (!/^https?:\/\//.test(alternative))
|
||||
continue;
|
||||
|
||||
let weight = 1;
|
||||
let match = /;q=([\d\.]+)$/.exec(alternative);
|
||||
if (match)
|
||||
{
|
||||
weight = parseFloat(match[1]);
|
||||
if (isNaN(weight) || !isFinite(weight) || weight < 0)
|
||||
weight = 1;
|
||||
if (weight > 10)
|
||||
weight = 10;
|
||||
|
||||
alternative = alternative.substr(0, match.index);
|
||||
}
|
||||
options.push([weight, alternative]);
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
let choice = Math.random() * totalWeight;
|
||||
for each (let [weight, alternative] in options)
|
||||
{
|
||||
choice -= weight;
|
||||
if (choice < 0)
|
||||
{
|
||||
loadFrom = alternative;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
isBaseLocation = (loadFrom == url);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ignore modification date if we are downloading from a different location
|
||||
forceDownload = true;
|
||||
}
|
||||
loadFrom = loadFrom.replace(/%VERSION%/, "ABP" + "3.5.0");
|
||||
|
||||
let request = null;
|
||||
function errorCallback(error)
|
||||
{
|
||||
let channelStatus = -1;
|
||||
try
|
||||
{
|
||||
channelStatus = request.channel.status;
|
||||
} catch (e) {}
|
||||
let responseStatus = "";
|
||||
try
|
||||
{
|
||||
responseStatus = request.channel.QueryInterface(Ci.nsIHttpChannel).responseStatus;
|
||||
} catch (e) {}
|
||||
setError(subscription, error, channelStatus, responseStatus, loadFrom, isBaseLocation, manual);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
|
||||
request.mozBackgroundRequest = true;
|
||||
request.open("GET", loadFrom);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
errorCallback("synchronize_invalid_url");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
request.overrideMimeType("text/plain");
|
||||
request.channel.loadFlags = request.channel.loadFlags |
|
||||
request.channel.INHIBIT_CACHING |
|
||||
request.channel.VALIDATE_ALWAYS;
|
||||
|
||||
// Override redirect limit from preferences, user might have set it to 1
|
||||
if (request.channel instanceof Ci.nsIHttpChannel)
|
||||
request.channel.redirectionLimit = 5;
|
||||
|
||||
var oldNotifications = request.channel.notificationCallbacks;
|
||||
var oldEventSink = null;
|
||||
request.channel.notificationCallbacks =
|
||||
{
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor, Ci.nsIChannelEventSink]),
|
||||
|
||||
getInterface: function(iid)
|
||||
{
|
||||
if (iid.equals(Ci.nsIChannelEventSink))
|
||||
{
|
||||
try {
|
||||
oldEventSink = oldNotifications.QueryInterface(iid);
|
||||
} catch(e) {}
|
||||
return this;
|
||||
}
|
||||
|
||||
if (oldNotifications)
|
||||
return oldNotifications.QueryInterface(iid);
|
||||
else
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
|
||||
asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback)
|
||||
{
|
||||
if (isBaseLocation && !hadTemporaryRedirect && oldChannel instanceof Ci.nsIHttpChannel)
|
||||
{
|
||||
try
|
||||
{
|
||||
subscription.alternativeLocations = oldChannel.getResponseHeader("X-Alternative-Locations");
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
subscription.alternativeLocations = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & Ci.nsIChannelEventSink.REDIRECT_TEMPORARY)
|
||||
hadTemporaryRedirect = true;
|
||||
else if (!hadTemporaryRedirect)
|
||||
newURL = newChannel.URI.spec;
|
||||
|
||||
if (oldEventSink)
|
||||
oldEventSink.asyncOnChannelRedirect(oldChannel, newChannel, flags, callback);
|
||||
else
|
||||
callback.onRedirectVerifyCallback(Cr.NS_OK);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError(e)
|
||||
}
|
||||
|
||||
if (subscription.lastModified && !forceDownload)
|
||||
request.setRequestHeader("If-Modified-Since", subscription.lastModified);
|
||||
|
||||
request.addEventListener("error", function(ev)
|
||||
{
|
||||
delete executing[url];
|
||||
try {
|
||||
request.channel.notificationCallbacks = null;
|
||||
} catch (e) {}
|
||||
|
||||
errorCallback("synchronize_connection_error");
|
||||
}, false);
|
||||
|
||||
request.addEventListener("load", function(ev)
|
||||
{
|
||||
delete executing[url];
|
||||
try {
|
||||
request.channel.notificationCallbacks = null;
|
||||
} catch (e) {}
|
||||
|
||||
// Status will be 0 for non-HTTP requests
|
||||
if (request.status && request.status != 200 && request.status != 304)
|
||||
{
|
||||
errorCallback("synchronize_connection_error");
|
||||
return;
|
||||
}
|
||||
|
||||
let newFilters = null;
|
||||
if (request.status != 304)
|
||||
{
|
||||
newFilters = readFilters(subscription, request.responseText, errorCallback);
|
||||
if (!newFilters)
|
||||
return;
|
||||
|
||||
subscription.lastModified = request.getResponseHeader("Last-Modified");
|
||||
}
|
||||
|
||||
if (isBaseLocation && !hadTemporaryRedirect)
|
||||
subscription.alternativeLocations = request.getResponseHeader("X-Alternative-Locations");
|
||||
subscription.lastSuccess = subscription.lastDownload = Math.round(Date.now() / MILLISECONDS_IN_SECOND);
|
||||
subscription.downloadStatus = "synchronize_ok";
|
||||
subscription.errors = 0;
|
||||
|
||||
// Expiration header is relative to server time - use Date header if it exists, otherwise local time
|
||||
let now = Math.round((new Date(request.getResponseHeader("Date")).getTime() || Date.now()) / MILLISECONDS_IN_SECOND);
|
||||
let expires = Math.round(new Date(request.getResponseHeader("Expires")).getTime() / MILLISECONDS_IN_SECOND) || 0;
|
||||
let expirationInterval = (expires ? expires - now : 0);
|
||||
for each (let filter in newFilters || subscription.filters)
|
||||
{
|
||||
if (!(filter instanceof CommentFilter))
|
||||
continue;
|
||||
|
||||
let match = /\bExpires\s*(?::|after)\s*(\d+)\s*(h)?/i.exec(filter.text);
|
||||
if (match)
|
||||
{
|
||||
let interval = parseInt(match[1], 10);
|
||||
if (match[2])
|
||||
interval *= SECONDS_IN_HOUR;
|
||||
else
|
||||
interval *= SECONDS_IN_DAY;
|
||||
|
||||
if (interval > expirationInterval)
|
||||
expirationInterval = interval;
|
||||
}
|
||||
}
|
||||
|
||||
// Expiration interval should be within allowed range
|
||||
expirationInterval = Math.min(Math.max(expirationInterval, MIN_EXPIRATION_INTERVAL), MAX_EXPIRATION_INTERVAL);
|
||||
|
||||
// Hard expiration: download immediately after twice the expiration interval
|
||||
subscription.expires = (subscription.lastDownload + expirationInterval * 2);
|
||||
|
||||
// Soft expiration: use random interval factor between 0.8 and 1.2
|
||||
subscription.softExpiration = (subscription.lastDownload + Math.round(expirationInterval * (Math.random() * 0.4 + 0.8)));
|
||||
|
||||
// Process some special filters and remove them
|
||||
if (newFilters)
|
||||
{
|
||||
let fixedTitle = false;
|
||||
for (let i = 0; i < newFilters.length; i++)
|
||||
{
|
||||
let filter = newFilters[i];
|
||||
if (!(filter instanceof CommentFilter))
|
||||
continue;
|
||||
|
||||
let match = /^!\s*(\w+)\s*:\s*(.*)/.exec(filter.text);
|
||||
if (match)
|
||||
{
|
||||
let keyword = match[1].toLowerCase();
|
||||
let value = match[2];
|
||||
let known = true;
|
||||
if (keyword == "redirect")
|
||||
{
|
||||
if (isBaseLocation && value != url)
|
||||
subscription.nextURL = value;
|
||||
}
|
||||
else if (keyword == "homepage")
|
||||
{
|
||||
let uri = Utils.makeURI(value);
|
||||
if (uri && (uri.scheme == "http" || uri.scheme == "https"))
|
||||
subscription.homepage = uri.spec;
|
||||
}
|
||||
else if (keyword == "title")
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
subscription.title = value;
|
||||
fixedTitle = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
known = false;
|
||||
|
||||
if (known)
|
||||
newFilters.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
subscription.fixedTitle = fixedTitle;
|
||||
}
|
||||
|
||||
if (isBaseLocation && newURL && newURL != url)
|
||||
{
|
||||
let listed = (subscription.url in FilterStorage.knownSubscriptions);
|
||||
if (listed)
|
||||
FilterStorage.removeSubscription(subscription);
|
||||
|
||||
url = newURL;
|
||||
|
||||
let newSubscription = Subscription.fromURL(url);
|
||||
for (let key in newSubscription)
|
||||
delete newSubscription[key];
|
||||
for (let key in subscription)
|
||||
newSubscription[key] = subscription[key];
|
||||
|
||||
delete Subscription.knownSubscriptions[subscription.url];
|
||||
newSubscription.oldSubscription = subscription;
|
||||
subscription = newSubscription;
|
||||
subscription.url = url;
|
||||
|
||||
if (!(subscription.url in FilterStorage.knownSubscriptions) && listed)
|
||||
FilterStorage.addSubscription(subscription);
|
||||
}
|
||||
|
||||
if (newFilters)
|
||||
FilterStorage.updateSubscriptionFilters(subscription, newFilters);
|
||||
delete subscription.oldSubscription;
|
||||
}, false);
|
||||
|
||||
executing[url] = true;
|
||||
FilterNotifier.triggerListeners("subscription.downloadStatus", subscription);
|
||||
|
||||
try
|
||||
{
|
||||
request.send(null);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
delete executing[url];
|
||||
errorCallback("synchronize_connection_error");
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether any subscriptions need to be downloaded and starts the download
|
||||
* if necessary.
|
||||
*/
|
||||
function checkSubscriptions()
|
||||
{
|
||||
if (!Prefs.subscriptions_autoupdate)
|
||||
return;
|
||||
|
||||
let time = Math.round(Date.now() / MILLISECONDS_IN_SECOND);
|
||||
for each (let subscription in FilterStorage.subscriptions)
|
||||
{
|
||||
if (!(subscription instanceof DownloadableSubscription))
|
||||
continue;
|
||||
|
||||
if (subscription.lastCheck && time - subscription.lastCheck > MAX_ABSENSE_INTERVAL)
|
||||
{
|
||||
// No checks for a long time interval - user must have been offline, e.g.
|
||||
// during a weekend. Increase soft expiration to prevent load peaks on the
|
||||
// server.
|
||||
subscription.softExpiration += time - subscription.lastCheck;
|
||||
}
|
||||
subscription.lastCheck = time;
|
||||
|
||||
// Sanity check: do expiration times make sense? Make sure people changing
|
||||
// system clock don't get stuck with outdated subscriptions.
|
||||
if (subscription.expires - time > MAX_EXPIRATION_INTERVAL)
|
||||
subscription.expires = time + MAX_EXPIRATION_INTERVAL;
|
||||
if (subscription.softExpiration - time > MAX_EXPIRATION_INTERVAL)
|
||||
subscription.softExpiration = time + MAX_EXPIRATION_INTERVAL;
|
||||
|
||||
if (subscription.softExpiration > time && subscription.expires > time)
|
||||
continue;
|
||||
|
||||
// Do not retry downloads more often than MIN_EXPIRATION_INTERVAL
|
||||
if (time - subscription.lastDownload >= MIN_EXPIRATION_INTERVAL)
|
||||
Synchronizer.execute(subscription, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a list of filters from text returned by a server.
|
||||
* @param {DownloadableSubscription} subscription subscription the info should be placed into
|
||||
* @param {String} text server response
|
||||
* @param {Function} errorCallback function to be called on error
|
||||
* @return {Array of Filter}
|
||||
*/
|
||||
function readFilters(subscription, text, errorCallback)
|
||||
{
|
||||
let lines = text.split(/[\r\n]+/);
|
||||
let match = /\[Adblock(?:\s*Plus\s*([\d\.]+)?)?\]/i.exec(lines[0]);
|
||||
if (!match)
|
||||
{
|
||||
errorCallback("synchronize_invalid_data");
|
||||
return null;
|
||||
}
|
||||
let minVersion = match[1];
|
||||
|
||||
for (let i = 0; i < lines.length; i++)
|
||||
{
|
||||
let match = /!\s*checksum[\s\-:]+([\w\+\/]+)/i.exec(lines[i]);
|
||||
if (match)
|
||||
{
|
||||
lines.splice(i, 1);
|
||||
let checksum = Utils.generateChecksum(lines);
|
||||
|
||||
if (checksum && checksum != match[1])
|
||||
{
|
||||
errorCallback("synchronize_checksum_mismatch");
|
||||
return null;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
delete subscription.requiredVersion;
|
||||
delete subscription.upgradeRequired;
|
||||
if (minVersion)
|
||||
{
|
||||
subscription.requiredVersion = minVersion;
|
||||
if (Utils.versionComparator.compare(minVersion, "3.5.0") > 0)
|
||||
subscription.upgradeRequired = true;
|
||||
}
|
||||
|
||||
lines.shift();
|
||||
let result = [];
|
||||
for each (let line in lines)
|
||||
{
|
||||
let filter = Filter.fromText(Filter.normalize(line));
|
||||
if (filter)
|
||||
result.push(filter);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an error during a subscription download.
|
||||
* @param {DownloadableSubscription} subscription subscription that failed to download
|
||||
* @param {Integer} channelStatus result code of the download channel
|
||||
* @param {String} responseStatus result code as received from server
|
||||
* @param {String} downloadURL the URL used for download
|
||||
* @param {String} error error ID in global.properties
|
||||
* @param {Boolean} isBaseLocation false if the subscription was downloaded from a location specified in X-Alternative-Locations header
|
||||
* @param {Boolean} manual true for a manually started download (should not trigger fallback requests)
|
||||
*/
|
||||
function setError(subscription, error, channelStatus, responseStatus, downloadURL, isBaseLocation, manual)
|
||||
{
|
||||
// If download from an alternative location failed, reset the list of
|
||||
// alternative locations - have to get an updated list from base location.
|
||||
if (!isBaseLocation)
|
||||
subscription.alternativeLocations = null;
|
||||
|
||||
try {
|
||||
Cu.reportError("Adblock Plus: Downloading filter subscription " + subscription.title + " failed (" + Utils.getString(error) + ")\n" +
|
||||
"Download address: " + downloadURL + "\n" +
|
||||
"Channel status: " + channelStatus + "\n" +
|
||||
"Server response: " + responseStatus);
|
||||
} catch(e) {}
|
||||
|
||||
subscription.lastDownload = Math.round(Date.now() / MILLISECONDS_IN_SECOND);
|
||||
subscription.downloadStatus = error;
|
||||
|
||||
// Request fallback URL if necessary - for automatic updates only
|
||||
if (!manual)
|
||||
{
|
||||
if (error == "synchronize_checksum_mismatch")
|
||||
{
|
||||
// No fallback for successful download with checksum mismatch, reset error counter
|
||||
subscription.errors = 0;
|
||||
}
|
||||
else
|
||||
subscription.errors++;
|
||||
|
||||
if (subscription.errors >= Prefs.subscriptions_fallbackerrors && /^https?:\/\//i.test(subscription.url))
|
||||
{
|
||||
subscription.errors = 0;
|
||||
|
||||
let fallbackURL = Prefs.subscriptions_fallbackurl;
|
||||
fallbackURL = fallbackURL.replace(/%VERSION%/g, encodeURIComponent("3.5.0"));
|
||||
fallbackURL = fallbackURL.replace(/%SUBSCRIPTION%/g, encodeURIComponent(subscription.url));
|
||||
fallbackURL = fallbackURL.replace(/%URL%/g, encodeURIComponent(downloadURL));
|
||||
fallbackURL = fallbackURL.replace(/%ERROR%/g, encodeURIComponent(error));
|
||||
fallbackURL = fallbackURL.replace(/%CHANNELSTATUS%/g, encodeURIComponent(channelStatus));
|
||||
fallbackURL = fallbackURL.replace(/%RESPONSESTATUS%/g, encodeURIComponent(responseStatus));
|
||||
|
||||
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
|
||||
request.mozBackgroundRequest = true;
|
||||
request.open("GET", fallbackURL);
|
||||
request.overrideMimeType("text/plain");
|
||||
request.channel.loadFlags = request.channel.loadFlags |
|
||||
request.channel.INHIBIT_CACHING |
|
||||
request.channel.VALIDATE_ALWAYS;
|
||||
request.addEventListener("load", function(ev)
|
||||
{
|
||||
if (!(subscription.url in FilterStorage.knownSubscriptions))
|
||||
return;
|
||||
|
||||
let match = /^(\d+)(?:\s+(\S+))?$/.exec(request.responseText);
|
||||
if (match && match[1] == "301" && match[2]) // Moved permanently
|
||||
subscription.nextURL = match[2];
|
||||
else if (match && match[1] == "410") // Gone
|
||||
{
|
||||
let data = "[Adblock]\n" + subscription.filters.map(function(f) f.text).join("\n");
|
||||
let url = "data:text/plain," + encodeURIComponent(data);
|
||||
let newSubscription = Subscription.fromURL(url);
|
||||
newSubscription.title = subscription.title;
|
||||
newSubscription.disabled = subscription.disabled;
|
||||
FilterStorage.removeSubscription(subscription);
|
||||
FilterStorage.addSubscription(newSubscription);
|
||||
Synchronizer.execute(newSubscription);
|
||||
}
|
||||
}, false);
|
||||
request.send(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Debugging module used for load time measurements.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["TimeLine"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let nestingCounter = 0;
|
||||
let firstTimeStamp = null;
|
||||
let lastTimeStamp = null;
|
||||
|
||||
let asyncActions = {__proto__: null};
|
||||
|
||||
/**
|
||||
* Time logging module, used to measure startup time of Adblock Plus (development builds only).
|
||||
* @class
|
||||
*/
|
||||
var TimeLine = {
|
||||
/**
|
||||
* Logs an event to console together with the time it took to get there.
|
||||
*/
|
||||
log: function(/**String*/ message, /**Boolean*/ _forceDisplay)
|
||||
{
|
||||
if (!_forceDisplay && nestingCounter <= 0)
|
||||
return;
|
||||
|
||||
let now = Date.now();
|
||||
let diff = lastTimeStamp ? Math.round(now - lastTimeStamp) : "first event";
|
||||
lastTimeStamp = now;
|
||||
|
||||
// Indent message depending on current nesting level
|
||||
for (let i = 0; i < nestingCounter; i++)
|
||||
message = "* " + message;
|
||||
|
||||
// Pad message with spaces
|
||||
let padding = [];
|
||||
for (let i = message.toString().length; i < 80; i++)
|
||||
padding.push(" ");
|
||||
dump("[" + now + "] ABP timeline: " + message + padding.join("") + "\t (" + diff + ")\n");
|
||||
},
|
||||
|
||||
/**
|
||||
* Called to indicate that application entered a block that needs to be timed.
|
||||
*/
|
||||
enter: function(/**String*/ message)
|
||||
{
|
||||
if (nestingCounter <= 0)
|
||||
firstTimeStamp = Date.now();
|
||||
|
||||
this.log(message, true);
|
||||
nestingCounter = (nestingCounter <= 0 ? 1 : nestingCounter + 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when application exited a block that TimeLine.enter() was called for.
|
||||
* @param {String} message message to be logged
|
||||
* @param {String} [asyncAction] identifier of a pending async action
|
||||
*/
|
||||
leave: function(message, asyncAction)
|
||||
{
|
||||
if (typeof asyncAction != "undefined")
|
||||
message += " (async action pending)";
|
||||
|
||||
nestingCounter--;
|
||||
this.log(message, true);
|
||||
|
||||
if (nestingCounter <= 0)
|
||||
{
|
||||
if (firstTimeStamp !== null)
|
||||
dump("ABP timeline: Total time elapsed: " + Math.round(Date.now() - firstTimeStamp) + "\n");
|
||||
firstTimeStamp = null;
|
||||
lastTimeStamp = null;
|
||||
}
|
||||
|
||||
if (typeof asyncAction != "undefined")
|
||||
{
|
||||
if (asyncAction in asyncActions)
|
||||
dump("ABP timeline: Warning: Async action " + asyncAction + " already executing\n");
|
||||
asyncActions[asyncAction] = {start: Date.now(), total: 0};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the application starts processing of an async action.
|
||||
*/
|
||||
asyncStart: function(/**String*/ asyncAction)
|
||||
{
|
||||
if (asyncAction in asyncActions)
|
||||
{
|
||||
let action = asyncActions[asyncAction];
|
||||
if ("currentStart" in action)
|
||||
dump("ABP timeline: Warning: Processing reentered for async action " + asyncAction + "\n");
|
||||
action.currentStart = Date.now();
|
||||
}
|
||||
else
|
||||
dump("ABP timeline: Warning: Async action " + asyncAction + " is unknown\n");
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the application finishes processing of an async action.
|
||||
*/
|
||||
asyncEnd: function(/**String*/ asyncAction)
|
||||
{
|
||||
if (asyncAction in asyncActions)
|
||||
{
|
||||
let action = asyncActions[asyncAction];
|
||||
if ("currentStart" in action)
|
||||
{
|
||||
action.total += Date.now() - action.currentStart;
|
||||
delete action.currentStart;
|
||||
}
|
||||
else
|
||||
dump("ABP timeline: Warning: Processing not entered for async action " + asyncAction + "\n");
|
||||
}
|
||||
else
|
||||
dump("ABP timeline: Warning: Async action " + asyncAction + " is unknown\n");
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when an async action is done and its time can be logged.
|
||||
*/
|
||||
asyncDone: function(/**String*/ asyncAction)
|
||||
{
|
||||
if (asyncAction in asyncActions)
|
||||
{
|
||||
let action = asyncActions[asyncAction];
|
||||
let now = Date.now();
|
||||
let diff = now - action.start;
|
||||
if ("currentStart" in action)
|
||||
dump("ABP timeline: Warning: Still processing for async action " + asyncAction + "\n");
|
||||
|
||||
let message = "Async action " + asyncAction + " done";
|
||||
let padding = [];
|
||||
for (let i = message.toString().length; i < 80; i++)
|
||||
padding.push(" ");
|
||||
dump("[" + now + "] ABP timeline: " + message + padding.join("") + "\t (" + action.total + "/" + diff + ")\n");
|
||||
}
|
||||
else
|
||||
dump("ABP timeline: Warning: Async action " + asyncAction + " is unknown\n");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,820 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#filter substitution
|
||||
|
||||
/**
|
||||
* @fileOverview Module containing a bunch of utility functions.
|
||||
*/
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Utils", "Cache"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
let sidebarParams = null;
|
||||
|
||||
/**
|
||||
* Provides a bunch of utility functions.
|
||||
* @class
|
||||
*/
|
||||
var Utils =
|
||||
{
|
||||
/**
|
||||
* Returns the add-on ID used by Adblock Plus
|
||||
*/
|
||||
get addonID()
|
||||
{
|
||||
return "@ADDON_ID@";
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the installed Adblock Plus version
|
||||
*/
|
||||
get addonVersion()
|
||||
{
|
||||
let version = "@ADDON_VERSION@";
|
||||
return (version[0] == "{" ? "99.9" : version);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the VCS revision used for this Adblock Plus build
|
||||
*/
|
||||
get addonBuild()
|
||||
{
|
||||
let build = "";
|
||||
return (build[0] == "{" ? "" : build);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns ID of the application
|
||||
*/
|
||||
get appID()
|
||||
{
|
||||
let id = Services.appinfo.ID;
|
||||
Utils.__defineGetter__("appID", function() id);
|
||||
return Utils.appID;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether we are running in Fennec, for Fennec-specific hacks
|
||||
* @type Boolean
|
||||
*/
|
||||
get isFennec()
|
||||
{
|
||||
let result = (this.appID == "{a23983c0-fd0e-11dc-95ff-0800200c9a66}" || this.appID == "{aa3c5121-dab2-40e2-81ca-7ea25febc110}");
|
||||
Utils.__defineGetter__("isFennec", function() result);
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the user interface locale selected for adblockplus chrome package.
|
||||
*/
|
||||
get appLocale()
|
||||
{
|
||||
let locale = "en-US";
|
||||
try
|
||||
{
|
||||
locale = Utils.chromeRegistry.getSelectedLocale("@ADDON_CHROME_NAME@");
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
}
|
||||
Utils.__defineGetter__("appLocale", function() locale);
|
||||
return Utils.appLocale;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns version of the Gecko platform
|
||||
*/
|
||||
get platformVersion()
|
||||
{
|
||||
let platformVersion = Services.appinfo.platformVersion;
|
||||
Utils.__defineGetter__("platformVersion", function() platformVersion);
|
||||
return Utils.platformVersion;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves a string from global.properties string bundle, will throw if string isn't found.
|
||||
*
|
||||
* @param {String} name string name
|
||||
* @return {String}
|
||||
*/
|
||||
getString: function(name)
|
||||
{
|
||||
let stringBundle = Services.strings.createBundle("chrome://@ADDON_CHROME_NAME@/locale/global.properties");
|
||||
Utils.getString = function(name)
|
||||
{
|
||||
return stringBundle.GetStringFromName(name);
|
||||
}
|
||||
return Utils.getString(name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows an alert message like window.alert() but with a custom title.
|
||||
*
|
||||
* @param {Window} parentWindow parent window of the dialog (can be null)
|
||||
* @param {String} message message to be displayed
|
||||
* @param {String} [title] dialog title, default title will be used if omitted
|
||||
*/
|
||||
alert: function(parentWindow, message, title)
|
||||
{
|
||||
if (!title)
|
||||
title = Utils.getString("default_dialog_title");
|
||||
Utils.promptService.alert(parentWindow, title, message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Asks the user for a confirmation like window.confirm() but with a custom title.
|
||||
*
|
||||
* @param {Window} parentWindow parent window of the dialog (can be null)
|
||||
* @param {String} message message to be displayed
|
||||
* @param {String} [title] dialog title, default title will be used if omitted
|
||||
* @return {Bool}
|
||||
*/
|
||||
confirm: function(parentWindow, message, title)
|
||||
{
|
||||
if (!title)
|
||||
title = Utils.getString("default_dialog_title");
|
||||
return Utils.promptService.confirm(parentWindow, title, message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the window for a document node.
|
||||
* @return {Window} will be null if the node isn't associated with a window
|
||||
*/
|
||||
getWindow: function(/**Node*/ node)
|
||||
{
|
||||
if ("ownerDocument" in node && node.ownerDocument)
|
||||
node = node.ownerDocument;
|
||||
|
||||
if ("defaultView" in node)
|
||||
return node.defaultView;
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* If the window doesn't have its own security context (e.g. about:blank or
|
||||
* data: URL) walks up the parent chain until a window is found that has a
|
||||
* security context.
|
||||
*/
|
||||
getOriginWindow: function(/**Window*/ wnd) /**Window*/
|
||||
{
|
||||
while (wnd != wnd.parent)
|
||||
{
|
||||
let uri = Utils.makeURI(wnd.location.href);
|
||||
if (uri.spec != "about:blank" && uri.spec != "moz-safe-about:blank" &&
|
||||
!Utils.netUtils.URIChainHasFlags(uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT))
|
||||
{
|
||||
break;
|
||||
}
|
||||
wnd = wnd.parent;
|
||||
}
|
||||
return wnd;
|
||||
},
|
||||
|
||||
/**
|
||||
* If a protocol using nested URIs like jar: is used - retrieves innermost
|
||||
* nested URI.
|
||||
*/
|
||||
unwrapURL: function(/**nsIURI or String*/ url) /**nsIURI*/
|
||||
{
|
||||
if (!(url instanceof Ci.nsIURI))
|
||||
url = Utils.makeURI(url);
|
||||
|
||||
if (url instanceof Ci.nsINestedURI)
|
||||
return url.innermostURI;
|
||||
else
|
||||
return url;
|
||||
},
|
||||
|
||||
/**
|
||||
* Translates a string URI into its nsIURI representation, will return null for
|
||||
* invalid URIs.
|
||||
*/
|
||||
makeURI: function(/**String*/ url) /**nsIURI*/
|
||||
{
|
||||
try
|
||||
{
|
||||
return Utils.ioService.newURI(url, null, null);
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts an action to the event queue of the current thread to run it
|
||||
* asynchronously. Any additional parameters to this function are passed
|
||||
* as parameters to the callback.
|
||||
*/
|
||||
runAsync: function(/**Function*/ callback, /**Object*/ thisPtr)
|
||||
{
|
||||
let params = Array.prototype.slice.call(arguments, 2);
|
||||
let runnable = {
|
||||
run: function()
|
||||
{
|
||||
callback.apply(thisPtr, params);
|
||||
}
|
||||
};
|
||||
Utils.threadManager.currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the DOM window associated with a particular request (if any).
|
||||
*/
|
||||
getRequestWindow: function(/**nsIChannel*/ channel) /**nsIDOMWindow*/
|
||||
{
|
||||
try
|
||||
{
|
||||
if (channel.notificationCallbacks)
|
||||
return channel.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow;
|
||||
} catch(e) {}
|
||||
|
||||
try
|
||||
{
|
||||
if (channel.loadGroup && channel.loadGroup.notificationCallbacks)
|
||||
return channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow;
|
||||
} catch(e) {}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates filter subscription checksum.
|
||||
*
|
||||
* @param {Array of String} lines filter subscription lines (with checksum line removed)
|
||||
* @return {String} checksum or null
|
||||
*/
|
||||
generateChecksum: function(lines)
|
||||
{
|
||||
let stream = null;
|
||||
try
|
||||
{
|
||||
// Checksum is an MD5 checksum (base64-encoded without the trailing "=") of
|
||||
// all lines in UTF-8 without the checksum line, joined with "\n".
|
||||
|
||||
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
stream = converter.convertToInputStream(lines.join("\n"));
|
||||
|
||||
let hashEngine = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
|
||||
hashEngine.init(hashEngine.MD5);
|
||||
hashEngine.updateFromStream(stream, stream.available());
|
||||
return hashEngine.finish(true).replace(/=+$/, "");
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (stream)
|
||||
stream.close();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens filter preferences dialog or focuses an already open dialog.
|
||||
* @param {Filter} [filter] filter to be selected
|
||||
*/
|
||||
openFiltersDialog: function(filter)
|
||||
{
|
||||
var dlg = Utils.windowMediator.getMostRecentWindow("abp:filters");
|
||||
if (dlg)
|
||||
{
|
||||
try
|
||||
{
|
||||
dlg.focus();
|
||||
}
|
||||
catch (e) {}
|
||||
if (filter)
|
||||
dlg.SubscriptionActions.selectFilter(filter);
|
||||
}
|
||||
else
|
||||
{
|
||||
Utils.windowWatcher.openWindow(null, "chrome://@ADDON_CHROME_NAME@/content/filters.xul", "_blank", "chrome,centerscreen,resizable,dialog=no", {wrappedJSObject: filter});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens a URL in the browser window. If browser window isn't passed as parameter,
|
||||
* this function attempts to find a browser window. If an event is passed in
|
||||
* it should be passed in to the browser if possible (will e.g. open a tab in
|
||||
* background depending on modifiers keys).
|
||||
*/
|
||||
loadInBrowser: function(/**String*/ url, /**Window*/ currentWindow, /**Event*/ event)
|
||||
{
|
||||
let abpHooks = currentWindow ? currentWindow.document.getElementById("abp-hooks") : null;
|
||||
if (!abpHooks || !abpHooks.addTab)
|
||||
{
|
||||
let enumerator = Utils.windowMediator.getZOrderDOMWindowEnumerator(null, true);
|
||||
if (!enumerator.hasMoreElements())
|
||||
{
|
||||
// On Linux the list returned will be empty, see bug 156333. Fall back to random order.
|
||||
enumerator = Utils.windowMediator.getEnumerator(null);
|
||||
}
|
||||
while (enumerator.hasMoreElements())
|
||||
{
|
||||
let window = enumerator.getNext().QueryInterface(Ci.nsIDOMWindow);
|
||||
abpHooks = window.document.getElementById("abp-hooks");
|
||||
if (abpHooks && abpHooks.addTab)
|
||||
{
|
||||
if (!currentWindow)
|
||||
window.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (abpHooks && abpHooks.addTab)
|
||||
abpHooks.addTab(url, event);
|
||||
else
|
||||
{
|
||||
let protocolService = Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(Ci.nsIExternalProtocolService);
|
||||
protocolService.loadURI(Utils.makeURI(url), null);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens a pre-defined documentation link in the browser window. This will
|
||||
* send the UI language to adblockplus.org so that the correct language
|
||||
* version of the page can be selected.
|
||||
*/
|
||||
loadDocLink: function(/**String*/ linkID)
|
||||
{
|
||||
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
|
||||
Cu.import(baseURL + "Prefs.jsm");
|
||||
|
||||
let link = Prefs.documentation_link.replace(/%LINK%/g, linkID).replace(/%LANG%/g, Utils.appLocale);
|
||||
Utils.loadInBrowser(link);
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats a unix time according to user's locale.
|
||||
* @param {Integer} time unix time in milliseconds
|
||||
* @return {String} formatted date and time
|
||||
*/
|
||||
formatTime: function(time)
|
||||
{
|
||||
try
|
||||
{
|
||||
let date = new Date(time);
|
||||
return Utils.dateFormatter.FormatDateTime("", Ci.nsIScriptableDateFormat.dateFormatShort,
|
||||
Ci.nsIScriptableDateFormat.timeFormatNoSeconds,
|
||||
date.getFullYear(), date.getMonth() + 1, date.getDate(),
|
||||
date.getHours(), date.getMinutes(), date.getSeconds());
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
// Make sure to return even on errors
|
||||
Cu.reportError(e);
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether any of the prefixes listed match the application locale,
|
||||
* returns matching prefix if any.
|
||||
*/
|
||||
checkLocalePrefixMatch: function(/**String*/ prefixes) /**String*/
|
||||
{
|
||||
if (!prefixes)
|
||||
return null;
|
||||
|
||||
let appLocale = Utils.appLocale;
|
||||
for each (let prefix in prefixes.split(/,/))
|
||||
if (new RegExp("^" + prefix + "\\b").test(appLocale))
|
||||
return prefix;
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Chooses the best filter subscription for user's language.
|
||||
* XXX: Removed code that will select at random
|
||||
*/
|
||||
chooseFilterSubscription: function(/**NodeList*/ subscriptions) /**Node*/
|
||||
{
|
||||
let selectedItem = null;
|
||||
let selectedPrefix = null;
|
||||
let matchCount = 0;
|
||||
for (let i = 0; i < subscriptions.length; i++)
|
||||
{
|
||||
let subscription = subscriptions[i];
|
||||
if (!selectedItem)
|
||||
selectedItem = subscription;
|
||||
|
||||
let prefix = Utils.checkLocalePrefixMatch(subscription.getAttribute("prefixes"));
|
||||
if (prefix)
|
||||
{
|
||||
if (!selectedPrefix || selectedPrefix.length < prefix.length)
|
||||
{
|
||||
selectedItem = subscription;
|
||||
selectedPrefix = prefix;
|
||||
matchCount = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedItem;
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves sidebar state before detaching/reattaching
|
||||
*/
|
||||
setParams: function(params)
|
||||
{
|
||||
sidebarParams = params;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves and removes sidebar state after detaching/reattaching
|
||||
*/
|
||||
getParams: function()
|
||||
{
|
||||
let ret = sidebarParams;
|
||||
sidebarParams = null;
|
||||
return ret;
|
||||
},
|
||||
|
||||
/**
|
||||
* Randomly generated class for collapsed nodes.
|
||||
* @type String
|
||||
*/
|
||||
collapsedClass: null,
|
||||
|
||||
/**
|
||||
* Nodes scheduled for post-processing (might be null).
|
||||
* @type Array of Node
|
||||
*/
|
||||
scheduledNodes: null,
|
||||
|
||||
/**
|
||||
* Schedules a node for post-processing.
|
||||
*/
|
||||
schedulePostProcess: function(node)
|
||||
{
|
||||
if (Utils.scheduledNodes)
|
||||
Utils.scheduledNodes.push(node);
|
||||
else
|
||||
{
|
||||
Utils.scheduledNodes = [node];
|
||||
Utils.runAsync(Utils.postProcessNodes);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Processes nodes scheduled for post-processing (typically hides them).
|
||||
*/
|
||||
postProcessNodes: function()
|
||||
{
|
||||
let nodes = Utils.scheduledNodes;
|
||||
Utils.scheduledNodes = null;
|
||||
|
||||
for each (let node in nodes)
|
||||
{
|
||||
// adjust frameset's cols/rows for frames
|
||||
let parentNode = node.parentNode;
|
||||
if (parentNode && parentNode instanceof Ci.nsIDOMHTMLFrameSetElement)
|
||||
{
|
||||
let hasCols = (parentNode.cols && parentNode.cols.indexOf(",") > 0);
|
||||
let hasRows = (parentNode.rows && parentNode.rows.indexOf(",") > 0);
|
||||
if ((hasCols || hasRows) && !(hasCols && hasRows))
|
||||
{
|
||||
let index = -1;
|
||||
for (let frame = node; frame; frame = frame.previousSibling)
|
||||
if (frame instanceof Ci.nsIDOMHTMLFrameElement || frame instanceof Ci.nsIDOMHTMLFrameSetElement)
|
||||
index++;
|
||||
|
||||
let property = (hasCols ? "cols" : "rows");
|
||||
let weights = parentNode[property].split(",");
|
||||
weights[index] = "0";
|
||||
parentNode[property] = weights.join(",");
|
||||
}
|
||||
}
|
||||
else
|
||||
node.className += " " + Utils.collapsedClass;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies RSA signature. The public key and signature should be base64-encoded.
|
||||
*/
|
||||
verifySignature: function(/**String*/ key, /**String*/ signature, /**String*/ data) /**Boolean*/
|
||||
{
|
||||
if (!Utils.crypto)
|
||||
return false;
|
||||
|
||||
// Maybe we did the same check recently, look it up in the cache
|
||||
if (!("_cache" in Utils.verifySignature))
|
||||
Utils.verifySignature._cache = new Cache(5);
|
||||
let cache = Utils.verifySignature._cache;
|
||||
let cacheKey = key + " " + signature + " " + data;
|
||||
if (cacheKey in cache.data)
|
||||
return cache.data[cacheKey];
|
||||
else
|
||||
cache.add(cacheKey, false);
|
||||
|
||||
let keyInfo, pubKey, context;
|
||||
try
|
||||
{
|
||||
let keyItem = Utils.crypto.getSECItem(atob(key));
|
||||
keyInfo = Utils.crypto.SECKEY_DecodeDERSubjectPublicKeyInfo(keyItem.address());
|
||||
if (keyInfo.isNull())
|
||||
throw new Error("SECKEY_DecodeDERSubjectPublicKeyInfo failed");
|
||||
|
||||
pubKey = Utils.crypto.SECKEY_ExtractPublicKey(keyInfo);
|
||||
if (pubKey.isNull())
|
||||
throw new Error("SECKEY_ExtractPublicKey failed");
|
||||
|
||||
let signatureItem = Utils.crypto.getSECItem(atob(signature));
|
||||
|
||||
context = Utils.crypto.VFY_CreateContext(pubKey, signatureItem.address(), Utils.crypto.SEC_OID_ISO_SHA_WITH_RSA_SIGNATURE, null);
|
||||
if (context.isNull())
|
||||
return false; // This could happen if the signature is invalid
|
||||
|
||||
let error = Utils.crypto.VFY_Begin(context);
|
||||
if (error < 0)
|
||||
throw new Error("VFY_Begin failed");
|
||||
|
||||
error = Utils.crypto.VFY_Update(context, data, data.length);
|
||||
if (error < 0)
|
||||
throw new Error("VFY_Update failed");
|
||||
|
||||
error = Utils.crypto.VFY_End(context);
|
||||
if (error < 0)
|
||||
return false;
|
||||
|
||||
cache.data[cacheKey] = true;
|
||||
return true;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (keyInfo && !keyInfo.isNull())
|
||||
Utils.crypto.SECKEY_DestroySubjectPublicKeyInfo(keyInfo);
|
||||
if (pubKey && !pubKey.isNull())
|
||||
Utils.crypto.SECKEY_DestroyPublicKey(pubKey);
|
||||
if (context && !context.isNull())
|
||||
Utils.crypto.VFY_DestroyContext(context, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A cache with a fixed capacity, newer entries replace entries that have been
|
||||
* stored first.
|
||||
* @constructor
|
||||
*/
|
||||
function Cache(/**Integer*/ size)
|
||||
{
|
||||
this._ringBuffer = new Array(size);
|
||||
this.data = {__proto__: null};
|
||||
}
|
||||
Cache.prototype =
|
||||
{
|
||||
/**
|
||||
* Ring buffer storing hash keys, allows determining which keys need to be
|
||||
* evicted.
|
||||
* @type Array
|
||||
*/
|
||||
_ringBuffer: null,
|
||||
|
||||
/**
|
||||
* Index in the ring buffer to be written next.
|
||||
* @type Integer
|
||||
*/
|
||||
_bufferIndex: 0,
|
||||
|
||||
/**
|
||||
* Cache data, maps values to the keys. Read-only access, for writing use
|
||||
* add() method.
|
||||
* @type Object
|
||||
*/
|
||||
data: null,
|
||||
|
||||
/**
|
||||
* Adds a key and the corresponding value to the cache.
|
||||
*/
|
||||
add: function(/**String*/ key, value)
|
||||
{
|
||||
if (!(key in this.data))
|
||||
{
|
||||
// This is a new key - we need to add it to the ring buffer and evict
|
||||
// another entry instead.
|
||||
let oldKey = this._ringBuffer[this._bufferIndex];
|
||||
if (typeof oldKey != "undefined")
|
||||
delete this.data[oldKey];
|
||||
this._ringBuffer[this._bufferIndex] = key;
|
||||
|
||||
this._bufferIndex++;
|
||||
if (this._bufferIndex >= this._ringBuffer.length)
|
||||
this._bufferIndex = 0;
|
||||
}
|
||||
|
||||
this.data[key] = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears cache contents.
|
||||
*/
|
||||
clear: function()
|
||||
{
|
||||
this._ringBuffer = new Array(this._ringBuffer.length);
|
||||
this.data = {__proto__: null};
|
||||
}
|
||||
}
|
||||
|
||||
// Getters for common services, this should be replaced by Services.jsm in future
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "categoryManager", "@mozilla.org/categorymanager;1", "nsICategoryManager");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "ioService", "@mozilla.org/network/io-service;1", "nsIIOService");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "threadManager", "@mozilla.org/thread-manager;1", "nsIThreadManager");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "promptService", "@mozilla.org/embedcomp/prompt-service;1", "nsIPromptService");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "effectiveTLD", "@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "netUtils", "@mozilla.org/network/util;1", "nsINetUtil");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "styleService", "@mozilla.org/content/style-sheet-service;1", "nsIStyleSheetService");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "prefService", "@mozilla.org/preferences-service;1", "nsIPrefService");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "versionComparator", "@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "windowMediator", "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "windowWatcher", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "chromeRegistry", "@mozilla.org/chrome/chrome-registry;1", "nsIXULChromeRegistry");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "systemPrincipal", "@mozilla.org/systemprincipal;1", "nsIPrincipal");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "dateFormatter", "@mozilla.org/intl/scriptabledateformat;1", "nsIScriptableDateFormat");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "childMessageManager", "@mozilla.org/childprocessmessagemanager;1", "nsISyncMessageSender");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "parentMessageManager", "@mozilla.org/parentprocessmessagemanager;1", "nsIFrameMessageManager");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "httpProtocol", "@mozilla.org/network/protocol;1?name=http", "nsIHttpProtocolHandler");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "clipboard", "@mozilla.org/widget/clipboard;1", "nsIClipboard");
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
|
||||
XPCOMUtils.defineLazyGetter(Utils, "crypto", function()
|
||||
{
|
||||
try
|
||||
{
|
||||
let ctypes = Components.utils.import("resource://gre/modules/ctypes.jsm", null).ctypes;
|
||||
|
||||
let nsslib = ctypes.open(ctypes.libraryName("nss3"));
|
||||
|
||||
let result = {};
|
||||
|
||||
// seccomon.h
|
||||
result.siUTF8String = 14;
|
||||
|
||||
// secoidt.h
|
||||
result.SEC_OID_ISO_SHA_WITH_RSA_SIGNATURE = 15;
|
||||
|
||||
// The following types are opaque to us
|
||||
result.VFYContext = ctypes.void_t;
|
||||
result.SECKEYPublicKey = ctypes.void_t;
|
||||
result.CERTSubjectPublicKeyInfo = ctypes.void_t;
|
||||
|
||||
/*
|
||||
* seccomon.h
|
||||
* struct SECItemStr {
|
||||
* SECItemType type;
|
||||
* unsigned char *data;
|
||||
* unsigned int len;
|
||||
* };
|
||||
*/
|
||||
result.SECItem = ctypes.StructType("SECItem", [
|
||||
{type: ctypes.int},
|
||||
{data: ctypes.unsigned_char.ptr},
|
||||
{len: ctypes.int}
|
||||
]);
|
||||
|
||||
/*
|
||||
* cryptohi.h
|
||||
* extern VFYContext *VFY_CreateContext(SECKEYPublicKey *key, SECItem *sig,
|
||||
* SECOidTag sigAlg, void *wincx);
|
||||
*/
|
||||
result.VFY_CreateContext = nsslib.declare(
|
||||
"VFY_CreateContext",
|
||||
ctypes.default_abi, result.VFYContext.ptr,
|
||||
result.SECKEYPublicKey.ptr,
|
||||
result.SECItem.ptr,
|
||||
ctypes.int,
|
||||
ctypes.voidptr_t
|
||||
);
|
||||
|
||||
/*
|
||||
* cryptohi.h
|
||||
* extern void VFY_DestroyContext(VFYContext *cx, PRBool freeit);
|
||||
*/
|
||||
result.VFY_DestroyContext = nsslib.declare(
|
||||
"VFY_DestroyContext",
|
||||
ctypes.default_abi, ctypes.void_t,
|
||||
result.VFYContext.ptr,
|
||||
ctypes.bool
|
||||
);
|
||||
|
||||
/*
|
||||
* cryptohi.h
|
||||
* extern SECStatus VFY_Begin(VFYContext *cx);
|
||||
*/
|
||||
result.VFY_Begin = nsslib.declare("VFY_Begin",
|
||||
ctypes.default_abi, ctypes.int,
|
||||
result.VFYContext.ptr
|
||||
);
|
||||
|
||||
/*
|
||||
* cryptohi.h
|
||||
* extern SECStatus VFY_Update(VFYContext *cx, const unsigned char *input,
|
||||
* unsigned int inputLen);
|
||||
*/
|
||||
result.VFY_Update = nsslib.declare(
|
||||
"VFY_Update",
|
||||
ctypes.default_abi, ctypes.int,
|
||||
result.VFYContext.ptr,
|
||||
ctypes.unsigned_char.ptr,
|
||||
ctypes.int
|
||||
);
|
||||
|
||||
/*
|
||||
* cryptohi.h
|
||||
* extern SECStatus VFY_End(VFYContext *cx);
|
||||
*/
|
||||
result.VFY_End = nsslib.declare(
|
||||
"VFY_End",
|
||||
ctypes.default_abi, ctypes.int,
|
||||
result.VFYContext.ptr
|
||||
);
|
||||
|
||||
/*
|
||||
* keyhi.h
|
||||
* extern CERTSubjectPublicKeyInfo *
|
||||
* SECKEY_DecodeDERSubjectPublicKeyInfo(SECItem *spkider);
|
||||
*/
|
||||
result.SECKEY_DecodeDERSubjectPublicKeyInfo = nsslib.declare(
|
||||
"SECKEY_DecodeDERSubjectPublicKeyInfo",
|
||||
ctypes.default_abi, result.CERTSubjectPublicKeyInfo.ptr,
|
||||
result.SECItem.ptr
|
||||
);
|
||||
|
||||
/*
|
||||
* keyhi.h
|
||||
* extern void SECKEY_DestroySubjectPublicKeyInfo(CERTSubjectPublicKeyInfo *spki);
|
||||
*/
|
||||
result.SECKEY_DestroySubjectPublicKeyInfo = nsslib.declare(
|
||||
"SECKEY_DestroySubjectPublicKeyInfo",
|
||||
ctypes.default_abi, ctypes.void_t,
|
||||
result.CERTSubjectPublicKeyInfo.ptr
|
||||
);
|
||||
|
||||
/*
|
||||
* keyhi.h
|
||||
* extern SECKEYPublicKey *
|
||||
* SECKEY_ExtractPublicKey(CERTSubjectPublicKeyInfo *);
|
||||
*/
|
||||
result.SECKEY_ExtractPublicKey = nsslib.declare(
|
||||
"SECKEY_ExtractPublicKey",
|
||||
ctypes.default_abi, result.SECKEYPublicKey.ptr,
|
||||
result.CERTSubjectPublicKeyInfo.ptr
|
||||
);
|
||||
|
||||
/*
|
||||
* keyhi.h
|
||||
* extern void SECKEY_DestroyPublicKey(SECKEYPublicKey *key);
|
||||
*/
|
||||
result.SECKEY_DestroyPublicKey = nsslib.declare(
|
||||
"SECKEY_DestroyPublicKey",
|
||||
ctypes.default_abi, ctypes.void_t,
|
||||
result.SECKEYPublicKey.ptr
|
||||
);
|
||||
|
||||
// Convenience method
|
||||
result.getSECItem = function(data)
|
||||
{
|
||||
var dataArray = new ctypes.ArrayType(ctypes.unsigned_char, data.length)();
|
||||
for (let i = 0; i < data.length; i++)
|
||||
dataArray[i] = data.charCodeAt(i) % 256;
|
||||
return new result.SECItem(result.siUTF8String, dataArray, dataArray.length);
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Cu.reportError(e);
|
||||
// Expected, ctypes isn't supported in Gecko 1.9.2
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if ("@mozilla.org/messenger/headerparser;1" in Cc)
|
||||
XPCOMUtils.defineLazyServiceGetter(Utils, "headerParser", "@mozilla.org/messenger/headerparser;1", "nsIMsgHeaderParser");
|
||||
else
|
||||
Utils.headerParser = null;
|
||||
@@ -0,0 +1,29 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
EXTRA_PP_JS_MODULES += [
|
||||
'AppIntegration.jsm',
|
||||
'Bootstrap.jsm',
|
||||
'ContentPolicy.jsm',
|
||||
'ContentPolicyRemote.jsm',
|
||||
'ElemHide.jsm',
|
||||
'ElemHideRemote.jsm',
|
||||
'FilterClasses.jsm',
|
||||
'FilterListener.jsm',
|
||||
'FilterNotifier.jsm',
|
||||
'FilterStorage.jsm',
|
||||
'IO.jsm',
|
||||
'Matcher.jsm',
|
||||
'ObjectTabs.jsm',
|
||||
'Prefs.jsm',
|
||||
'Public.jsm',
|
||||
'RequestNotifier.jsm',
|
||||
'SubscriptionClasses.jsm',
|
||||
'Sync.jsm',
|
||||
'Synchronizer.jsm',
|
||||
'TimeLine.jsm',
|
||||
'Utils.jsm',
|
||||
]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIRS += [
|
||||
'addon',
|
||||
'components',
|
||||
'content',
|
||||
'locale',
|
||||
'modules',
|
||||
'skin',
|
||||
]
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# We could use confvars.sh and configure.in but for some reason it gets a bit messy...
|
||||
# Just use what we have.. While UXP may reduce the importance of moz.configure we
|
||||
# likely will never be able to eliminate it entirely... Oh well.
|
||||
|
||||
# Disables building the platform code
|
||||
# NOTE: This negates any possiblity of an XPIDL or Binary XPCOM Components
|
||||
set_config('MOZ_DISABLE_PLATFORM', '1')
|
||||
|
||||
# Minimum required by moz.configure since we don't want everything in
|
||||
# toolkit/moz.configure when the platform isn't built
|
||||
set_config('MOZ_PACKAGER_FORMAT', 'omni')
|
||||
set_config('MOZ_JAR_MAKER_FILE_FORMAT', 'flat')
|
||||
|
||||
# moz.configure won't allow setting normal variables in a global context so
|
||||
# create a function that can be called to get specific info
|
||||
def confvars(key):
|
||||
metadata = {
|
||||
'name': 'ABPrime',
|
||||
'id': 'abprime@projects.binaryoutcast.com',
|
||||
'version': '1.0.7',
|
||||
'creator': 'Binary Outcast',
|
||||
'description': 'Bootstrapped adblocking is yesterday!',
|
||||
'slug': 'abprime',
|
||||
'chrome': 'abprime',
|
||||
'targetApp': 'Pale Moon',
|
||||
'targetID': '{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}',
|
||||
'targetMinVer': '27.0.0',
|
||||
'targetMaxVer': '28.*',
|
||||
'basilisk': '1'
|
||||
}
|
||||
|
||||
return metadata[key]
|
||||
|
||||
# These will set the config/substs
|
||||
set_config('ADDON_NAME', confvars('name'))
|
||||
set_config('ADDON_ID', confvars('id'))
|
||||
set_config('ADDON_VERSION', confvars('version'))
|
||||
set_config('ADDON_AUTHOR', confvars('creator'))
|
||||
set_config('ADDON_SHORT_DESC', confvars('description'))
|
||||
set_config('ADDON_XPI_NAME', confvars('slug'))
|
||||
set_config('ADDON_CHROME_NAME', confvars('chrome'))
|
||||
set_config('ADDON_TARGET_APP_NAME', confvars('targetApp'))
|
||||
set_config('ADDON_TARGET_APP_ID', confvars('targetID'))
|
||||
set_config('ADDON_TARGET_APP_MINVER', confvars('targetMinVer'))
|
||||
set_config('ADDON_TARGET_APP_MAXVER', confvars('targetMaxVer'))
|
||||
set_config('ADDON_TARGET_BASILISK', confvars('basilisk'))
|
||||
|
||||
# These will set defines
|
||||
# Should weed them down to just what is required which I believe
|
||||
# is just ADDON_TARGET_BASILISK
|
||||
set_define('ADDON_NAME', confvars('name'))
|
||||
set_define('ADDON_ID', confvars('id'))
|
||||
set_define('ADDON_VERSION', confvars('version'))
|
||||
set_define('ADDON_AUTHOR', confvars('creator'))
|
||||
set_define('ADDON_SHORT_DESC', confvars('description'))
|
||||
set_define('ADDON_XPI_NAME', confvars('slug'))
|
||||
set_define('ADDON_CHROME_NAME', confvars('chrome'))
|
||||
set_define('ADDON_TARGET_APP_NAME', confvars('targetApp'))
|
||||
set_define('ADDON_TARGET_APP_ID', confvars('targetID'))
|
||||
set_define('ADDON_TARGET_APP_MINVER', confvars('targetMinVer'))
|
||||
set_define('ADDON_TARGET_APP_MAXVER', confvars('targetMaxVer'))
|
||||
set_define('ADDON_TARGET_BASILISK', confvars('basilisk'))
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
#mainBox
|
||||
{
|
||||
visibility: hidden;
|
||||
}
|
||||
#mainBox[loaded]
|
||||
{
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#title
|
||||
{
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#mainGroup
|
||||
{
|
||||
margin-top: 15px;
|
||||
width: 450px;
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#homepageTitle, #authorsTitle, #contributorsTitle, #subscriptionAuthorsTitle, #translatorsTitle
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#description, #homepage, #authorsBox, #contributorsBox, #subscriptionAuthorsBox
|
||||
{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 356 B |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
@namespace html url("http://www.w3.org/1999/xhtml");
|
||||
|
||||
/*
|
||||
* Force left-to-right everywhere where we are displaying addresses
|
||||
*/
|
||||
.suggestion > .radio-label-box:-moz-locale-dir(rtl),
|
||||
html|*.textbox-input:-moz-locale-dir(rtl)
|
||||
{
|
||||
direction: ltr;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
#patternGroup {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#anchorGroup {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#typeGroupLabel {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#typeGroup {
|
||||
overflow: auto;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
:root:not([advancedMode="true"]) #options {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#disabledWarning, #groupDisabledWarning, #regexpWarning, #shortpatternWarning, #matchWarning {
|
||||
color: #E00000;
|
||||
}
|
||||
|
||||
#disabledWarning > *, #groupDisabledWarning > * {
|
||||
margin: 0px;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.text-link {
|
||||
font-size: 80%;
|
||||
-moz-user-focus: ignore;
|
||||
}
|
||||
|
||||
.help {
|
||||
color: #0000E0;
|
||||
border-bottom: 1px dotted #0000E0;
|
||||
cursor: help;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
tooltip {
|
||||
/* Gecko 1.8.1 doesn't support multiline tooltips :-( */
|
||||
max-width: none;
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
#buttons
|
||||
{
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#noSubscriptions
|
||||
{
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.localeMatch
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
.selectSubscriptionItem
|
||||
{
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.subscription
|
||||
{
|
||||
padding: 5px;
|
||||
}
|
||||
.subscription:not(:last-child)
|
||||
{
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.subscription:not([selected="true"]) > .disabled
|
||||
{
|
||||
}
|
||||
|
||||
.subscription:not([selected="true"]) > .disabled .titleBox
|
||||
{
|
||||
color: #808080;
|
||||
}
|
||||
.subscription:not([selected="true"]) > .disabled .status
|
||||
{
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.titleBox .title,
|
||||
.titleBox > .titleEditor
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.subscription description, .subscription textbox
|
||||
{
|
||||
margin: 0px !important;
|
||||
padding: 0px !important;
|
||||
border-width: 0px !important;
|
||||
-moz-appearance: none !important;
|
||||
}
|
||||
|
||||
.subscription .link
|
||||
{
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.subscription .warning
|
||||
{
|
||||
color: #FF0000;
|
||||
}
|
||||
|
||||
.enabledCheckbox
|
||||
{
|
||||
padding: 2px;
|
||||
-moz-margin-end: 10px;
|
||||
}
|
||||
.enabledCheckbox:focus
|
||||
{
|
||||
outline: 1px dotted gray;
|
||||
}
|
||||
.enabledCheckbox .checkbox-label-box
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.actionButton
|
||||
{
|
||||
font: -moz-info;
|
||||
}
|
||||
|
||||
splitter
|
||||
{
|
||||
border-width: 0px !important;
|
||||
}
|
||||
|
||||
#filtersTooltip
|
||||
{
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.tooltipLabel
|
||||
{
|
||||
font-weight: bold;
|
||||
-moz-margin-end: 10px;
|
||||
}
|
||||
|
||||
#tooltip-additional
|
||||
{
|
||||
color: #C00000;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
tree
|
||||
{
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#col-slow {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#col-hitcount, #col-lasthit {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#col-hitcount
|
||||
{
|
||||
min-width: 60px;
|
||||
}
|
||||
#col-enabled
|
||||
{
|
||||
min-width: 48px;
|
||||
}
|
||||
#col-slow
|
||||
{
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Force left-to-right for filter text but not comments
|
||||
*/
|
||||
treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-filter, type-invalid),
|
||||
treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-filter, type-whitelist),
|
||||
treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-filter, type-filterlist),
|
||||
treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-filter, type-elemhide)
|
||||
{
|
||||
direction: ltr;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-cell-text(col-filter, dummy-true)
|
||||
{
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-cell-text(col-filter, type-whitelist, selected-false)
|
||||
{
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-cell-text(col-filter, type-elemhide, selected-false)
|
||||
{
|
||||
color: #000080;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-cell-text(col-slow)
|
||||
{
|
||||
font-size: 0px;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-cell-text(col-filter, disabled-true, selected-false)
|
||||
{
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-cell-text(col-filter, type-comment, selected-false)
|
||||
{
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-cell-text(col-filter, type-invalid, selected-false)
|
||||
{
|
||||
color: #C00000;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-image(col-enabled, disabled-true)
|
||||
{
|
||||
list-style-image: url(checkbox.png);
|
||||
-moz-image-region: rect(13px 13px 26px 0px);
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-image(col-enabled, disabled-false)
|
||||
{
|
||||
list-style-image: url(checkbox.png);
|
||||
-moz-image-region: rect(0px 13px 13px 0px);
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-image(col-slow, slow-true)
|
||||
{
|
||||
list-style-image: url(slow.png);
|
||||
}
|
||||
|
||||
.findbar-highlight
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
:root
|
||||
{
|
||||
max-width: 500px;
|
||||
background-image: url(abp-icon-big.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 95% 5%;
|
||||
font-size: 130%;
|
||||
}
|
||||
|
||||
.sectionTitle
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#changeDescription > .text-link
|
||||
{
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#listNameContainer,
|
||||
#listNone
|
||||
{
|
||||
margin: 10px 40px;
|
||||
}
|
||||
|
||||
#listNone
|
||||
{
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#acceptableAds
|
||||
{
|
||||
font-size: 90%;
|
||||
font-weight: bold;
|
||||
}
|
||||
#acceptableAds > *
|
||||
{
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.sectionContainer
|
||||
{
|
||||
margin: 1em 2em;
|
||||
}
|
||||
|
After Width: | Height: | Size: 533 B |
@@ -0,0 +1,25 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#filter substitution
|
||||
|
||||
[.] chrome.jar:
|
||||
% skin @ADDON_CHROME_NAME@ classic/1.0 chrome/skin/
|
||||
skin/about.css
|
||||
skin/abp-icon-big.png
|
||||
skin/abp-status-16.png
|
||||
skin/abp-status.png
|
||||
skin/checkbox.png
|
||||
skin/close.png
|
||||
skin/composer.css
|
||||
skin/filters.css
|
||||
skin/firstRun.css
|
||||
skin/item-state.png
|
||||
skin/overlay.css
|
||||
skin/sendReport.css
|
||||
skin/sidebar.css
|
||||
skin/slow.png
|
||||
skin/subscriptionSelection.css
|
||||
|
||||
% style chrome://global/content/customizeToolbar.xul chrome://@ADDON_CHROME_NAME@/skin/overlay.css
|
||||
@@ -0,0 +1,6 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
#abp-status
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
toolbar[iconsize="small"] #abp-toolbarbutton,
|
||||
#PersonalToolbar #abp-toolbarbutton,
|
||||
#header-view-toolbar > #abp-toolbarbutton,
|
||||
#abp-status {
|
||||
list-style-image: url("abp-status-16.png");
|
||||
-moz-image-region: rect(0px, 16px, 16px, 0px);
|
||||
}
|
||||
toolbar[iconsize="small"] #abp-toolbarbutton[abpstate="disabled"],
|
||||
#PersonalToolbar #abp-toolbarbutton[abpstate="disabled"],
|
||||
#header-view-toolbar > #abp-toolbarbutton[abpstate="disabled"],
|
||||
#abp-status[abpstate="disabled"],
|
||||
toolbar[iconsize="small"] #abp-toolbarbutton[abpstate="whitelisted"],
|
||||
#PersonalToolbar #abp-toolbarbutton[abpstate="whitelisted"],
|
||||
#header-view-toolbar > #abp-toolbarbutton[abpstate="whitelisted"],
|
||||
#abp-status[abpstate="whitelisted"] {
|
||||
-moz-image-region: rect(16px, 16px, 32px, 0px);
|
||||
}
|
||||
|
||||
#abp-toolbar-popup {
|
||||
list-style-image: none;
|
||||
-moz-image-region: rect(0px, 0px, 0px, 0px);
|
||||
}
|
||||
|
||||
toolbox[vertical="true"] toolbar #abp-toolbarbutton dropmarker {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
menuitem[default="true"] {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#abp-toolbarbutton,
|
||||
#abp-site-info {
|
||||
list-style-image: url("abp-status.png");
|
||||
-moz-image-region: rect(0px, 24px, 24px, 0px);
|
||||
}
|
||||
#abp-toolbarbutton[abpstate="disabled"],
|
||||
#abp-toolbarbutton[abpstate="whitelisted"],
|
||||
#abp-site-info[abpaction="enable"],
|
||||
#abp-site-info[abpaction="enable_site"] {
|
||||
-moz-image-region: rect(24px, 24px, 48px, 0px);
|
||||
}
|
||||
|
||||
/* Hack: force the label to be displayed below icon for type="menu" */
|
||||
#abp-toolbarbutton[type="menu"]
|
||||
{
|
||||
-moz-box-orient: horizontal;
|
||||
}
|
||||
toolbar[mode="full"]:not([labelalign="end"]) #abp-toolbarbutton[type="menu"]
|
||||
{
|
||||
-moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#menu-vertical");
|
||||
}
|
||||
|
||||
/* Thunderbird-specific toolbar icon styles */
|
||||
#header-view-toolbar > #abp-toolbarbutton
|
||||
{
|
||||
-moz-appearance: dualbutton;
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
/* Hide toolbar icon text in Thunderbird to save space */
|
||||
#header-view-toolbar > #abp-toolbarbutton .toolbarbutton-text
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* SeaMonkey expects the icon to be rather large, add margin */
|
||||
#mail-toolbox #abp-toolbarbutton .toolbarbutton-icon
|
||||
{
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#abp-status-image {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#abp-site-info .pageaction-image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#abp-toolbarbutton > toolbarbutton {
|
||||
/* Argh, Songbird defines image region directly on the anonymous toolbarbutton element */
|
||||
-moz-image-region: inherit !important;
|
||||
}
|
||||
|
||||
#abp-tooltip {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#abp-tooltip label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
#abp-tooltip description:not([hidden="true"])+label {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#abp-sidebar-title {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
#abp-sidebar-toolbar {
|
||||
display: -moz-box !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
#abp-sidebar-close {
|
||||
padding: 4px 2px;
|
||||
border-style: none !important;
|
||||
-moz-user-focus: normal;
|
||||
list-style-image: url("close.png");
|
||||
-moz-appearance: none;
|
||||
-moz-image-region: rect(0px, 14px, 14px, 0px);
|
||||
}
|
||||
|
||||
#abp-sidebar-close:hover {
|
||||
-moz-image-region: rect(0px, 28px, 14px, 14px);
|
||||
}
|
||||
|
||||
#abp-sidebar-close:hover:active {
|
||||
-moz-image-region: rect(0px, 42px, 14px, 28px);
|
||||
}
|
||||
|
||||
.abp-contributebutton
|
||||
{
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.abp-contributebutton-btn
|
||||
{
|
||||
font: -moz-info;
|
||||
margin-left: 40px;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.abp-contributebutton-close
|
||||
{
|
||||
border-style: none !important;
|
||||
-moz-user-focus: normal;
|
||||
list-style-image: url("close.png");
|
||||
-moz-appearance: none;
|
||||
-moz-image-region: rect(0px, 14px, 14px, 0px);
|
||||
}
|
||||
|
||||
.abp-contributebutton-close:hover
|
||||
{
|
||||
-moz-image-region: rect(0px, 28px, 14px, 14px);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
.wizard-header
|
||||
{
|
||||
-moz-binding: url(chrome://adblockplus/content/sendReport.xul#headerBinding) !important;
|
||||
padding: 10px 5px !important;
|
||||
}
|
||||
|
||||
.progressLabel
|
||||
{
|
||||
margin: 5px 0px;
|
||||
text-align: center;
|
||||
font-size: 110%;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.progressLabel.active
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
progressmeter
|
||||
{
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.radioDescription
|
||||
{
|
||||
-moz-margin-start: 32px;
|
||||
}
|
||||
|
||||
radio, checkbox, .topLabel, #dataDeck
|
||||
{
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#recentReports
|
||||
{
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#recentReportsList
|
||||
{
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#outdatedSubscriptionsList
|
||||
{
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
#issuesBox
|
||||
{
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#issuesChangeMessage
|
||||
{
|
||||
color: red;
|
||||
}
|
||||
|
||||
#screenshotButtons
|
||||
{
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#screenshotBox
|
||||
{
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#commentLengthWarning
|
||||
{
|
||||
color: red;
|
||||
}
|
||||
|
||||
#commentLengthWarning[visible="false"]
|
||||
{
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/*
|
||||
* Force left-to-right everywhere where we are displaying addresses
|
||||
*/
|
||||
#data:-moz-locale-dir(rtl)
|
||||
{
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
#sendReportError
|
||||
{
|
||||
color: red;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
#sendReportErrorLinks, #typeWarningTextLink
|
||||
{
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#sendReportErrorBox
|
||||
{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
#suggestionsList {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#detachButton, #reattachButton:not([disabled="true"]) {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#reattachButton[disabled="true"] {
|
||||
color: GrayText;
|
||||
}
|
||||
|
||||
#detachButton, #reattachButton {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
tooltip {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#tooltipPreview {
|
||||
margin:10px;
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
#tooltip label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#contextBlock,
|
||||
#contextWhitelist {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#state {
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
#size {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
/*
|
||||
* Force left-to-right everywhere where we are displaying addresses
|
||||
*/
|
||||
treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-filter),
|
||||
treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-address),
|
||||
treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-size)
|
||||
{
|
||||
direction: ltr;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.disabledTextLabel
|
||||
{
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-cell-text(state-filtered, selected-false),
|
||||
treechildren::-moz-tree-cell-text(state-hidden, selected-false) {
|
||||
color: #C00000;
|
||||
}
|
||||
treechildren::-moz-tree-cell-text(state-whitelisted, selected-false) {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-image(col-state, dummy-false)
|
||||
{
|
||||
list-style-image: url(item-state.png);
|
||||
-moz-image-region: rect(0px 10px 10px 0px);
|
||||
-moz-margin-start: 3px;
|
||||
}
|
||||
treechildren::-moz-tree-image(col-state, filter-disabled-true, dummy-false) {
|
||||
-moz-image-region: rect(10px 10px 20px 0px);
|
||||
}
|
||||
treechildren::-moz-tree-image(col-state, state-filtered, dummy-false),
|
||||
treechildren::-moz-tree-image(col-state, state-hidden, dummy-false) {
|
||||
-moz-image-region: rect(20px 10px 30px 0px);
|
||||
}
|
||||
treechildren::-moz-tree-image(col-state, state-whitelisted, dummy-false) {
|
||||
-moz-image-region: rect(30px 10px 40px 0px);
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-cell-text(col-filter, state-hidden, selected-false) {
|
||||
color: #000080;
|
||||
}
|
||||
treechildren::-moz-tree-cell-text(col-filter, filter-disabled-true, selected-false) {
|
||||
color: #C0C0C0;
|
||||
}
|
||||
|
After Width: | Height: | Size: 718 B |
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* This Source Code is subject to the terms of the Mozilla Public License
|
||||
* version 2.0 (the "License"). You can obtain a copy of the License at
|
||||
* http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
dialog
|
||||
{
|
||||
width: 550px;
|
||||
}
|
||||
|
||||
*[invisible="true"]
|
||||
{
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#supplementMessage
|
||||
{
|
||||
color: #F00000;
|
||||
}
|
||||
|
||||
.localeMatch
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#all-subscriptions-loading
|
||||
{
|
||||
margin: 50px;
|
||||
}
|
||||
|
||||
#all-subscriptions
|
||||
{
|
||||
min-height: 200px;
|
||||
}
|
||||
#all-subscriptions > richlistitem > .variant
|
||||
{
|
||||
width: 200px;
|
||||
}
|
||||
#all-subscriptions > richlistitem:not(:first-child) > .subscriptionTitle,
|
||||
#all-subscriptions > richlistitem:not(:first-child) > .subscriptionTitle + .variant
|
||||
{
|
||||
border-top: 1px dashed black;
|
||||
margin-top: 0px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
#supplementMessage
|
||||
{
|
||||
margin-top: 5px;
|
||||
}
|
||||
#supplementMessage > label
|
||||
{
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
if CONFIG['MOZ_COMPOSER']:
|
||||
DIRS += ['/editor/ui']
|
||||
|
||||
if CONFIG['MOZ_MAILNEWS']:
|
||||
DIRS += [
|
||||
'/modules/ldap',
|
||||
'/modules/mork',
|
||||
'/mailnews',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_CALENDAR']:
|
||||
DIRS += [
|
||||
'/modules/libical',
|
||||
'/calendar/lightning',
|
||||
'/calendar/timezones'
|
||||
]
|
||||
|
||||
DIRS += ['/communicator']
|
||||
@@ -0,0 +1,14 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
pref("inspector.blink.border-color", "#CC0000");
|
||||
pref("inspector.blink.border-width", 2);
|
||||
pref("inspector.blink.duration", 1200);
|
||||
pref("inspector.blink.on", true);
|
||||
pref("inspector.blink.speed", 100);
|
||||
pref("inspector.blink.invert", false);
|
||||
pref("inspector.dom.showAnon", true);
|
||||
pref("inspector.dom.showWhitespaceNodes", true);
|
||||
pref("inspector.dom.showAccessibleNodes", false);
|
||||
pref("inspector.dom.showProcessingInstructions", true);
|
||||