summaryrefslogtreecommitdiffstats
path: root/toolkit/components
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components')
-rw-r--r--toolkit/components/blocklist/blocklist.manifest7
-rw-r--r--toolkit/components/blocklist/moz.build14
-rw-r--r--toolkit/components/blocklist/nsBlocklistService.js1666
-rw-r--r--toolkit/components/blocklist/nsBlocklistServiceContent.js113
-rw-r--r--toolkit/components/build/nsToolkitCompsModule.cpp2
-rw-r--r--toolkit/components/captivedetect/captivedetect.js2
-rw-r--r--toolkit/components/diskspacewatcher/DiskSpaceWatcher.cpp3
-rw-r--r--toolkit/components/jsdownloads/src/DownloadIntegration.jsm2
-rw-r--r--toolkit/components/jsdownloads/src/DownloadPlatform.cpp2
-rw-r--r--toolkit/components/moz.build1
-rw-r--r--toolkit/components/narrate/NarrateControls.jsm135
-rw-r--r--toolkit/components/narrate/Narrator.jsm78
-rw-r--r--toolkit/components/narrate/VoiceSelect.jsm23
-rw-r--r--toolkit/components/osfile/modules/osfile_async_front.jsm4
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_constants.js7
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js6
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_path_constants.js2
-rw-r--r--toolkit/components/printing/content/simplifyMode.css2
-rw-r--r--toolkit/components/reader/AboutReader.jsm409
-rw-r--r--toolkit/components/reader/JSDOMParser.js46
-rw-r--r--toolkit/components/reader/Readability.js853
-rw-r--r--toolkit/components/reader/ReaderMode.jsm312
-rw-r--r--toolkit/components/reader/ReaderWorker.js7
-rw-r--r--toolkit/components/reader/content/aboutReader.html73
-rw-r--r--toolkit/components/reader/content/aboutReader.js2
-rw-r--r--toolkit/components/startup/nsAppStartup.cpp4
-rw-r--r--toolkit/components/telemetry/Telemetry.cpp2
-rw-r--r--toolkit/components/telemetry/TelemetryEnvironment.jsm44
-rw-r--r--toolkit/components/telemetry/TelemetryHistogram.cpp38
-rw-r--r--toolkit/components/telemetry/TelemetrySession.jsm2
-rw-r--r--toolkit/components/telemetry/TelemetryStorage.jsm2
-rw-r--r--toolkit/components/telemetry/tests/unit/head.js2
32 files changed, 2799 insertions, 1066 deletions
diff --git a/toolkit/components/blocklist/blocklist.manifest b/toolkit/components/blocklist/blocklist.manifest
new file mode 100644
index 000000000..c770b4e7d
--- /dev/null
+++ b/toolkit/components/blocklist/blocklist.manifest
@@ -0,0 +1,7 @@
+component {66354bc9-7ed1-4692-ae1d-8da97d6b205e} nsBlocklistService.js process=main
+contract @mozilla.org/extensions/blocklist;1 {66354bc9-7ed1-4692-ae1d-8da97d6b205e} process=main
+category profile-after-change nsBlocklistService @mozilla.org/extensions/blocklist;1 process=main
+component {e0a106ed-6ad4-47a4-b6af-2f1c8aa4712d} nsBlocklistServiceContent.js process=content
+contract @mozilla.org/extensions/blocklist;1 {e0a106ed-6ad4-47a4-b6af-2f1c8aa4712d} process=content
+
+category update-timer nsBlocklistService @mozilla.org/extensions/blocklist;1,getService,blocklist-background-update-timer,extensions.blocklist.interval,86400 \ No newline at end of file
diff --git a/toolkit/components/blocklist/moz.build b/toolkit/components/blocklist/moz.build
new file mode 100644
index 000000000..3dc3be5ba
--- /dev/null
+++ b/toolkit/components/blocklist/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; 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/.
+
+EXTRA_COMPONENTS += [
+ 'blocklist.manifest',
+ 'nsBlocklistServiceContent.js',
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'nsBlocklistService.js',
+]
diff --git a/toolkit/components/blocklist/nsBlocklistService.js b/toolkit/components/blocklist/nsBlocklistService.js
new file mode 100644
index 000000000..891346b72
--- /dev/null
+++ b/toolkit/components/blocklist/nsBlocklistService.js
@@ -0,0 +1,1666 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+
+/* 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+try {
+ // AddonManager.jsm doesn't allow itself to be imported in the child
+ // process. We're used in the child process (for now), so guard against
+ // this.
+ Components.utils.import("resource://gre/modules/AddonManager.jsm");
+ /* globals AddonManagerPrivate*/
+} catch (e) {
+}
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+#ifdef MOZ_WEBEXTENSIONS
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+#else
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
+ "resource://gre/modules/UpdateChannel.jsm");
+#endif
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest",
+ "resource://gre/modules/ServiceRequest.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+const TOOLKIT_ID = "toolkit@mozilla.org";
+const KEY_PROFILEDIR = "ProfD";
+const KEY_APPDIR = "XCurProcD";
+const FILE_BLOCKLIST = "blocklist.xml";
+const PREF_BLOCKLIST_LASTUPDATETIME = "app.update.lastUpdateTime.blocklist-background-update-timer";
+const PREF_BLOCKLIST_URL = "extensions.blocklist.url";
+const PREF_BLOCKLIST_ITEM_URL = "extensions.blocklist.itemURL";
+const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
+const PREF_BLOCKLIST_INTERVAL = "extensions.blocklist.interval";
+const PREF_BLOCKLIST_LEVEL = "extensions.blocklist.level";
+const PREF_BLOCKLIST_PINGCOUNTTOTAL = "extensions.blocklist.pingCountTotal";
+const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
+const PREF_BLOCKLIST_SUPPRESSUI = "extensions.blocklist.suppressUI";
+const PREF_ONECRL_VIA_AMO = "security.onecrl.via.amo";
+const PREF_BLOCKLIST_UPDATE_ENABLED = "services.blocklist.update_enabled";
+const PREF_GENERAL_USERAGENT_LOCALE = "general.useragent.locale";
+const PREF_APP_DISTRIBUTION = "distribution.id";
+const PREF_APP_DISTRIBUTION_VERSION = "distribution.version";
+const PREF_EM_LOGGING_ENABLED = "extensions.logging.enabled";
+const XMLURI_BLOCKLIST = "http://www.mozilla.org/2006/addons-blocklist";
+const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
+const UNKNOWN_XPCOM_ABI = "unknownABI";
+const URI_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul"
+const DEFAULT_SEVERITY = 3;
+const DEFAULT_LEVEL = 2;
+const MAX_BLOCK_LEVEL = 3;
+const SEVERITY_OUTDATED = 0;
+const VULNERABILITYSTATUS_NONE = 0;
+const VULNERABILITYSTATUS_UPDATE_AVAILABLE = 1;
+const VULNERABILITYSTATUS_NO_UPDATE = 2;
+
+const EXTENSION_BLOCK_FILTERS = ["id", "name", "creator", "homepageURL", "updateURL"];
+
+var gLoggingEnabled = null;
+var gBlocklistEnabled = true;
+var gBlocklistLevel = DEFAULT_LEVEL;
+
+XPCOMUtils.defineLazyServiceGetter(this, "gConsole",
+ "@mozilla.org/consoleservice;1",
+ "nsIConsoleService");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gVersionChecker",
+ "@mozilla.org/xpcom/version-comparator;1",
+ "nsIVersionComparator");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gCertBlocklistService",
+ "@mozilla.org/security/certblocklist;1",
+ "nsICertBlocklist");
+
+XPCOMUtils.defineLazyGetter(this, "gPref", function() {
+ return Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).
+ QueryInterface(Ci.nsIPrefBranch);
+});
+
+// From appinfo in Services.jsm. It is not possible to use the one in
+// Services.jsm since it will not successfully QueryInterface nsIXULAppInfo in
+// xpcshell tests due to other code calling Services.appinfo before the
+// nsIXULAppInfo is created by the tests.
+XPCOMUtils.defineLazyGetter(this, "gApp", function() {
+ let appinfo = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ try {
+ appinfo.QueryInterface(Ci.nsIXULAppInfo);
+ } catch (ex) {
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (!(ex instanceof Components.Exception) ||
+ ex.result != Cr.NS_NOINTERFACE)
+ throw ex;
+ }
+ return appinfo;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gABI", function() {
+ let abi = null;
+ try {
+ abi = gApp.XPCOMABI;
+ }
+ catch (e) {
+ LOG("BlockList Global gABI: XPCOM ABI unknown.");
+ }
+
+#ifdef XP_MACOSX
+ // Mac universal build should report a different ABI than either macppc
+ // or mactel.
+ let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"].
+ getService(Ci.nsIMacUtils);
+
+ if (macutils.isUniversalBinary)
+ abi += "-u-" + macutils.architecturesInBinary;
+#endif
+ return abi;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gOSVersion", function() {
+ let osVersion;
+ let sysInfo = Cc["@mozilla.org/system-info;1"].
+ getService(Ci.nsIPropertyBag2);
+ try {
+ osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version");
+ }
+ catch (e) {
+ LOG("BlockList Global gOSVersion: OS Version unknown.");
+ }
+
+ if (osVersion) {
+ try {
+ osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
+ }
+ catch (e) {
+ // Not all platforms have a secondary widget library, so an error is nothing to worry about.
+ }
+ osVersion = encodeURIComponent(osVersion);
+ }
+ return osVersion;
+});
+
+// shared code for suppressing bad cert dialogs
+XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() {
+ let temp = { };
+ Components.utils.import("resource://gre/modules/CertUtils.jsm", temp);
+ return temp;
+});
+
+/**
+ * Logs a string to the error console.
+ * @param string
+ * The string to write to the error console..
+ */
+function LOG(string) {
+ if (gLoggingEnabled) {
+ dump("*** " + string + "\n");
+ gConsole.logStringMessage(string);
+ }
+}
+
+/**
+ * Gets a preference value, handling the case where there is no default.
+ * @param func
+ * The name of the preference function to call, on nsIPrefBranch
+ * @param preference
+ * The name of the preference
+ * @param defaultValue
+ * The default value to return in the event the preference has
+ * no setting
+ * @returns The value of the preference, or undefined if there was no
+ * user or default value.
+ */
+function getPref(func, preference, defaultValue) {
+ try {
+ return gPref[func](preference);
+ }
+ catch (e) {
+ }
+ return defaultValue;
+}
+
+/**
+ * Constructs a URI to a spec.
+ * @param spec
+ * The spec to construct a URI to
+ * @returns The nsIURI constructed.
+ */
+function newURI(spec) {
+ var ioServ = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ return ioServ.newURI(spec, null, null);
+}
+
+// Restarts the application checking in with observers first
+function restartApp() {
+ // Notify all windows that an application quit has been requested.
+ var os = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
+ createInstance(Ci.nsISupportsPRBool);
+ os.notifyObservers(cancelQuit, "quit-application-requested", null);
+
+ // Something aborted the quit process.
+ if (cancelQuit.data)
+ return;
+
+ var as = Cc["@mozilla.org/toolkit/app-startup;1"].
+ getService(Ci.nsIAppStartup);
+ as.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
+}
+
+/**
+ * Checks whether this blocklist element is valid for the current OS and ABI.
+ * If the element has an "os" attribute then the current OS must appear in
+ * its comma separated list for the element to be valid. Similarly for the
+ * xpcomabi attribute.
+ */
+function matchesOSABI(blocklistElement) {
+ if (blocklistElement.hasAttribute("os")) {
+ var choices = blocklistElement.getAttribute("os").split(",");
+ if (choices.length > 0 && choices.indexOf(gApp.OS) < 0)
+ return false;
+ }
+
+ if (blocklistElement.hasAttribute("xpcomabi")) {
+ choices = blocklistElement.getAttribute("xpcomabi").split(",");
+ if (choices.length > 0 && choices.indexOf(gApp.XPCOMABI) < 0)
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Gets the current value of the locale. It's possible for this preference to
+ * be localized, so we have to do a little extra work here. Similar code
+ * exists in nsHttpHandler.cpp when building the UA string.
+ */
+function getLocale() {
+ try {
+ // Get the default branch
+ var defaultPrefs = gPref.getDefaultBranch(null);
+ return defaultPrefs.getComplexValue(PREF_GENERAL_USERAGENT_LOCALE,
+ Ci.nsIPrefLocalizedString).data;
+ } catch (e) {}
+
+ return gPref.getCharPref(PREF_GENERAL_USERAGENT_LOCALE);
+}
+
+/* Get the distribution pref values, from defaults only */
+function getDistributionPrefValue(aPrefName) {
+ var prefValue = "default";
+
+ var defaults = gPref.getDefaultBranch(null);
+ try {
+ prefValue = defaults.getCharPref(aPrefName);
+ } catch (e) {
+ // use default when pref not found
+ }
+
+ return prefValue;
+}
+
+/**
+ * Parse a string representation of a regular expression. Needed because we
+ * use the /pattern/flags form (because it's detectable), which is only
+ * supported as a literal in JS.
+ *
+ * @param aStr
+ * String representation of regexp
+ * @return RegExp instance
+ */
+function parseRegExp(aStr) {
+ let lastSlash = aStr.lastIndexOf("/");
+ let pattern = aStr.slice(1, lastSlash);
+ let flags = aStr.slice(lastSlash + 1);
+ return new RegExp(pattern, flags);
+}
+
+/**
+ * Manages the Blocklist. The Blocklist is a representation of the contents of
+ * blocklist.xml and allows us to remotely disable / re-enable blocklisted
+ * items managed by the Extension Manager with an item's appDisabled property.
+ * It also blocklists plugins with data from blocklist.xml.
+ */
+
+function Blocklist() {
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ Services.obs.addObserver(this, "sessionstore-windows-restored", false);
+ gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
+ gBlocklistEnabled = getPref("getBoolPref", PREF_BLOCKLIST_ENABLED, true);
+ gBlocklistLevel = Math.min(getPref("getIntPref", PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
+ MAX_BLOCK_LEVEL);
+ gPref.addObserver("extensions.blocklist.", this, false);
+ gPref.addObserver(PREF_EM_LOGGING_ENABLED, this, false);
+ this.wrappedJSObject = this;
+ // requests from child processes come in here, see receiveMessage.
+ Services.ppmm.addMessageListener("Blocklist:getPluginBlocklistState", this);
+ Services.ppmm.addMessageListener("Blocklist:content-blocklist-updated", this);
+}
+
+Blocklist.prototype = {
+ /**
+ * Extension ID -> array of Version Ranges
+ * Each value in the version range array is a JS Object that has the
+ * following properties:
+ * "minVersion" The minimum version in a version range (default = 0)
+ * "maxVersion" The maximum version in a version range (default = *)
+ * "targetApps" Application ID -> array of Version Ranges
+ * (default = current application ID)
+ * Each value in the version range array is a JS Object that
+ * has the following properties:
+ * "minVersion" The minimum version in a version range
+ * (default = 0)
+ * "maxVersion" The maximum version in a version range
+ * (default = *)
+ */
+ _addonEntries: null,
+ _gfxEntries: null,
+ _pluginEntries: null,
+
+ shutdown: function() {
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ Services.ppmm.removeMessageListener("Blocklist:getPluginBlocklistState", this);
+ Services.ppmm.removeMessageListener("Blocklist:content-blocklist-updated", this);
+ gPref.removeObserver("extensions.blocklist.", this);
+ gPref.removeObserver(PREF_EM_LOGGING_ENABLED, this);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "xpcom-shutdown":
+ this.shutdown();
+ break;
+ case "nsPref:changed":
+ switch (aData) {
+ case PREF_EM_LOGGING_ENABLED:
+ gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
+ break;
+ case PREF_BLOCKLIST_ENABLED:
+ gBlocklistEnabled = getPref("getBoolPref", PREF_BLOCKLIST_ENABLED, true);
+ this._loadBlocklist();
+ this._blocklistUpdated(null, null);
+ break;
+ case PREF_BLOCKLIST_LEVEL:
+ gBlocklistLevel = Math.min(getPref("getIntPref", PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
+ MAX_BLOCK_LEVEL);
+ this._blocklistUpdated(null, null);
+ break;
+ }
+ break;
+ case "sessionstore-windows-restored":
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ this._preloadBlocklist();
+ break;
+ }
+ },
+
+ // Message manager message handlers
+ receiveMessage: function(aMsg) {
+ switch (aMsg.name) {
+ case "Blocklist:getPluginBlocklistState":
+ return this.getPluginBlocklistState(aMsg.data.addonData,
+ aMsg.data.appVersion,
+ aMsg.data.toolkitVersion);
+ case "Blocklist:content-blocklist-updated":
+ Services.obs.notifyObservers(null, "content-blocklist-updated", null);
+ break;
+ default:
+ throw new Error("Unknown blocklist message received from content: " + aMsg.name);
+ }
+ return undefined;
+ },
+
+ /* See nsIBlocklistService */
+ isAddonBlocklisted: function(addon, appVersion, toolkitVersion) {
+ return this.getAddonBlocklistState(addon, appVersion, toolkitVersion) ==
+ Ci.nsIBlocklistService.STATE_BLOCKED;
+ },
+
+ /* See nsIBlocklistService */
+ getAddonBlocklistState: function(addon, appVersion, toolkitVersion) {
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+ return this._getAddonBlocklistState(addon, this._addonEntries,
+ appVersion, toolkitVersion);
+ },
+
+ /**
+ * Private version of getAddonBlocklistState that allows the caller to pass in
+ * the add-on blocklist entries to compare against.
+ *
+ * @param id
+ * The ID of the item to get the blocklist state for.
+ * @param version
+ * The version of the item to get the blocklist state for.
+ * @param addonEntries
+ * The add-on blocklist entries to compare against.
+ * @param appVersion
+ * The application version to compare to, will use the current
+ * version if null.
+ * @param toolkitVersion
+ * The toolkit version to compare to, will use the current version if
+ * null.
+ * @returns The blocklist state for the item, one of the STATE constants as
+ * defined in nsIBlocklistService.
+ */
+ _getAddonBlocklistState: function(addon, addonEntries, appVersion, toolkitVersion) {
+ if (!gBlocklistEnabled)
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (!appVersion && !gApp.version)
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+
+ if (!appVersion)
+ appVersion = gApp.version;
+ if (!toolkitVersion)
+ toolkitVersion = gApp.platformVersion;
+
+ var blItem = this._findMatchingAddonEntry(addonEntries, addon);
+ if (!blItem)
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+
+ for (let currentblItem of blItem.versions) {
+ if (currentblItem.includesItem(addon.version, appVersion, toolkitVersion))
+ return currentblItem.severity >= gBlocklistLevel ? Ci.nsIBlocklistService.STATE_BLOCKED :
+ Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
+ }
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ },
+
+ /**
+ * Returns the set of prefs of the add-on stored in the blocklist file
+ * (probably to revert them on disabling).
+ * @param addon
+ * The add-on whose to-be-reset prefs are to be found.
+ */
+ _getAddonPrefs: function(addon) {
+ let entry = this._findMatchingAddonEntry(this._addonEntries, addon);
+ return entry.prefs.slice(0);
+ },
+
+ _findMatchingAddonEntry: function(aAddonEntries, aAddon) {
+ if (!aAddon)
+ return null;
+ // Returns true if the params object passes the constraints set by entry.
+ // (For every non-null property in entry, the same key must exist in
+ // params and value must be the same)
+ function checkEntry(entry, params) {
+ for (let [key, value] of entry) {
+ if (value === null || value === undefined)
+ continue;
+ if (params[key]) {
+ if (value instanceof RegExp) {
+ if (!value.test(params[key])) {
+ return false;
+ }
+ } else if (value !== params[key]) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ let params = {};
+ for (let filter of EXTENSION_BLOCK_FILTERS) {
+ params[filter] = aAddon[filter];
+ }
+ if (params.creator)
+ params.creator = params.creator.name;
+ for (let entry of aAddonEntries) {
+ if (checkEntry(entry.attributes, params)) {
+ return entry;
+ }
+ }
+ return null;
+ },
+
+ /* See nsIBlocklistService */
+ getAddonBlocklistURL: function(addon, appVersion, toolkitVersion) {
+ if (!gBlocklistEnabled)
+ return "";
+
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+
+ let blItem = this._findMatchingAddonEntry(this._addonEntries, addon);
+ if (!blItem || !blItem.blockID)
+ return null;
+
+ return this._createBlocklistURL(blItem.blockID);
+ },
+
+ _createBlocklistURL: function(id) {
+ let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL);
+ url = url.replace(/%blockID%/g, id);
+
+ return url;
+ },
+
+ notify: function(aTimer) {
+ if (!gBlocklistEnabled)
+ return;
+
+ try {
+ var dsURI = gPref.getCharPref(PREF_BLOCKLIST_URL);
+ }
+ catch (e) {
+ LOG("Blocklist::notify: The " + PREF_BLOCKLIST_URL + " preference" +
+ " is missing!");
+ return;
+ }
+
+ var pingCountVersion = getPref("getIntPref", PREF_BLOCKLIST_PINGCOUNTVERSION, 0);
+ var pingCountTotal = getPref("getIntPref", PREF_BLOCKLIST_PINGCOUNTTOTAL, 1);
+ var daysSinceLastPing = 0;
+ if (pingCountVersion == 0) {
+ daysSinceLastPing = "new";
+ }
+ else {
+ // Seconds in one day is used because nsIUpdateTimerManager stores the
+ // last update time in seconds.
+ let secondsInDay = 60 * 60 * 24;
+ let lastUpdateTime = getPref("getIntPref", PREF_BLOCKLIST_LASTUPDATETIME, 0);
+ if (lastUpdateTime == 0) {
+ daysSinceLastPing = "invalid";
+ }
+ else {
+ let now = Math.round(Date.now() / 1000);
+ daysSinceLastPing = Math.floor((now - lastUpdateTime) / secondsInDay);
+ }
+
+ if (daysSinceLastPing == 0 || daysSinceLastPing == "invalid") {
+ pingCountVersion = pingCountTotal = "invalid";
+ }
+ }
+
+ if (pingCountVersion < 1)
+ pingCountVersion = 1;
+ if (pingCountTotal < 1)
+ pingCountTotal = 1;
+
+ dsURI = dsURI.replace(/%APP_ID%/g, gApp.ID);
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (gApp.version)
+ dsURI = dsURI.replace(/%APP_VERSION%/g, gApp.version);
+ dsURI = dsURI.replace(/%PRODUCT%/g, gApp.name);
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (gApp.version)
+ dsURI = dsURI.replace(/%VERSION%/g, gApp.version);
+ dsURI = dsURI.replace(/%BUILD_ID%/g, gApp.appBuildID);
+ dsURI = dsURI.replace(/%BUILD_TARGET%/g, gApp.OS + "_" + gABI);
+ dsURI = dsURI.replace(/%OS_VERSION%/g, gOSVersion);
+ dsURI = dsURI.replace(/%LOCALE%/g, getLocale());
+#ifdef MOZ_WEBEXTENSIONS
+ dsURI = dsURI.replace(/%CHANNEL%/g, UpdateUtils.UpdateChannel);
+#else
+ dsURI = dsURI.replace(/%CHANNEL%/g, UpdateChannel.get());
+#endif
+ dsURI = dsURI.replace(/%PLATFORM_VERSION%/g, gApp.platformVersion);
+ dsURI = dsURI.replace(/%DISTRIBUTION%/g,
+ getDistributionPrefValue(PREF_APP_DISTRIBUTION));
+ dsURI = dsURI.replace(/%DISTRIBUTION_VERSION%/g,
+ getDistributionPrefValue(PREF_APP_DISTRIBUTION_VERSION));
+ dsURI = dsURI.replace(/%PING_COUNT%/g, pingCountVersion);
+ dsURI = dsURI.replace(/%TOTAL_PING_COUNT%/g, pingCountTotal);
+ dsURI = dsURI.replace(/%DAYS_SINCE_LAST_PING%/g, daysSinceLastPing);
+ dsURI = dsURI.replace(/\+/g, "%2B");
+
+ // Under normal operations it will take around 5,883,516 years before the
+ // preferences used to store pingCountVersion and pingCountTotal will rollover
+ // so this code doesn't bother trying to do the "right thing" here.
+ if (pingCountVersion != "invalid") {
+ pingCountVersion++;
+ if (pingCountVersion > 2147483647) {
+ // Rollover to -1 if the value is greater than what is support by an
+ // integer preference. The -1 indicates that the counter has been reset.
+ pingCountVersion = -1;
+ }
+ gPref.setIntPref(PREF_BLOCKLIST_PINGCOUNTVERSION, pingCountVersion);
+ }
+
+ if (pingCountTotal != "invalid") {
+ pingCountTotal++;
+ if (pingCountTotal > 2147483647) {
+ // Rollover to 1 if the value is greater than what is support by an
+ // integer preference.
+ pingCountTotal = -1;
+ }
+ gPref.setIntPref(PREF_BLOCKLIST_PINGCOUNTTOTAL, pingCountTotal);
+ }
+
+ // Verify that the URI is valid
+ try {
+ var uri = newURI(dsURI);
+ }
+ catch (e) {
+ LOG("Blocklist::notify: There was an error creating the blocklist URI\r\n" +
+ "for: " + dsURI + ", error: " + e);
+ return;
+ }
+
+ LOG("Blocklist::notify: Requesting " + uri.spec);
+ let request = new ServiceRequest();
+ request.open("GET", uri.spec, true);
+ request.channel.notificationCallbacks = new gCertUtils.BadCertHandler();
+ request.overrideMimeType("text/xml");
+ request.setRequestHeader("Cache-Control", "no-cache");
+ request.QueryInterface(Components.interfaces.nsIJSXMLHttpRequest);
+
+ request.addEventListener("error", event => this.onXMLError(event), false);
+ request.addEventListener("load", event => this.onXMLLoad(event), false);
+ request.send(null);
+
+ // When the blocklist loads we need to compare it to the current copy so
+ // make sure we have loaded it.
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+ },
+
+ onXMLLoad: Task.async(function*(aEvent) {
+ let request = aEvent.target;
+ try {
+ gCertUtils.checkCert(request.channel);
+ }
+ catch (e) {
+ LOG("Blocklist::onXMLLoad: " + e);
+ return;
+ }
+ let responseXML = request.responseXML;
+ if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
+ (request.status != 200 && request.status != 0)) {
+ LOG("Blocklist::onXMLLoad: there was an error during load");
+ return;
+ }
+
+ var oldAddonEntries = this._addonEntries;
+ var oldPluginEntries = this._pluginEntries;
+ this._addonEntries = [];
+ this._gfxEntries = [];
+ this._pluginEntries = [];
+
+ this._loadBlocklistFromString(request.responseText);
+ // We don't inform the users when the graphics blocklist changed at runtime.
+ // However addons and plugins blocking status is refreshed.
+ this._blocklistUpdated(oldAddonEntries, oldPluginEntries);
+
+ try {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
+ yield OS.File.writeAtomic(path, request.responseText, {tmpPath: path + ".tmp"});
+ } catch (e) {
+ LOG("Blocklist::onXMLLoad: " + e);
+ }
+ }),
+
+ onXMLError: function(aEvent) {
+ try {
+ var request = aEvent.target;
+ // the following may throw (e.g. a local file or timeout)
+ var status = request.status;
+ }
+ catch (e) {
+ request = aEvent.target.channel.QueryInterface(Ci.nsIRequest);
+ status = request.status;
+ }
+ var statusText = "nsIXMLHttpRequest channel unavailable";
+ // When status is 0 we don't have a valid channel.
+ if (status != 0) {
+ try {
+ statusText = request.statusText;
+ } catch (e) {
+ }
+ }
+ LOG("Blocklist:onError: There was an error loading the blocklist file\r\n" +
+ statusText);
+ },
+
+ /**
+ * Finds the newest blocklist file from the application and the profile and
+ * load it or does nothing if neither exist.
+ */
+ _loadBlocklist: function() {
+ this._addonEntries = [];
+ this._gfxEntries = [];
+ this._pluginEntries = [];
+ var profFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
+ if (profFile.exists()) {
+ this._loadBlocklistFromFile(profFile);
+ return;
+ }
+ var appFile = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
+ if (appFile.exists()) {
+ this._loadBlocklistFromFile(appFile);
+ return;
+ }
+ LOG("Blocklist::_loadBlocklist: no XML File found");
+ },
+
+ /**
+# The blocklist XML file looks something like this:
+#
+# <blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+# <emItems>
+# <emItem id="item_1@domain" blockID="i1">
+# <prefs>
+# <pref>accessibility.accesskeycausesactivation</pref>
+# <pref>accessibility.blockautorefresh</pref>
+# </prefs>
+# <versionRange minVersion="1.0" maxVersion="2.0.*">
+# <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
+# <versionRange minVersion="1.5" maxVersion="1.5.*"/>
+# <versionRange minVersion="1.7" maxVersion="1.7.*"/>
+# </targetApplication>
+# <targetApplication id="toolkit@mozilla.org">
+# <versionRange minVersion="1.9" maxVersion="1.9.*"/>
+# </targetApplication>
+# </versionRange>
+# <versionRange minVersion="3.0" maxVersion="3.0.*">
+# <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
+# <versionRange minVersion="1.5" maxVersion="1.5.*"/>
+# </targetApplication>
+# <targetApplication id="toolkit@mozilla.org">
+# <versionRange minVersion="1.9" maxVersion="1.9.*"/>
+# </targetApplication>
+# </versionRange>
+# </emItem>
+# <emItem id="item_2@domain" blockID="i2">
+# <versionRange minVersion="3.1" maxVersion="4.*"/>
+# </emItem>
+# <emItem id="item_3@domain">
+# <versionRange>
+# <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
+# <versionRange minVersion="1.5" maxVersion="1.5.*"/>
+# </targetApplication>
+# </versionRange>
+# </emItem>
+# <emItem id="item_4@domain" blockID="i3">
+# <versionRange>
+# <targetApplication>
+# <versionRange minVersion="1.5" maxVersion="1.5.*"/>
+# </targetApplication>
+# </versionRange>
+# <emItem id="/@badperson\.com$/"/>
+# </emItems>
+# <pluginItems>
+# <pluginItem blockID="i4">
+# <!-- All match tags must match a plugin to blocklist a plugin -->
+# <match name="name" exp="some plugin"/>
+# <match name="description" exp="1[.]2[.]3"/>
+# </pluginItem>
+# </pluginItems>
+# <certItems>
+# <!-- issuerName is the DER issuer name data base64 encoded... -->
+# <certItem issuerName="MA0xCzAJBgNVBAMMAmNh">
+# <!-- ... as is the serial number DER data -->
+# <serialNumber>AkHVNA==</serialNumber>
+# </certItem>
+# <!-- subject is the DER subject name data base64 encoded... -->
+# <certItem subject="MA0xCzAJBgNVBAMMAmNh" pubKeyHash="/xeHA5s+i9/z9d8qy6JEuE1xGoRYIwgJuTE/lmaGJ7M=">
+# </certItem>
+# </certItems>
+# </blocklist>
+ */
+
+ _loadBlocklistFromFile: function(file) {
+ if (!gBlocklistEnabled) {
+ LOG("Blocklist::_loadBlocklistFromFile: blocklist is disabled");
+ return;
+ }
+
+ let telemetry = Services.telemetry;
+
+ if (this._isBlocklistPreloaded()) {
+ telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(false);
+ this._loadBlocklistFromString(this._preloadedBlocklistContent);
+ delete this._preloadedBlocklistContent;
+ return;
+ }
+
+ if (!file.exists()) {
+ LOG("Blocklist::_loadBlocklistFromFile: XML File does not exist " + file.path);
+ return;
+ }
+
+ telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(true);
+
+ let text = "";
+ let fstream = null;
+ let cstream = null;
+
+ try {
+ fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
+ .createInstance(Components.interfaces.nsIConverterInputStream);
+
+ fstream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ let str = {};
+ let read = 0;
+
+ do {
+ read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value
+ text += str.value;
+ } while (read != 0);
+ } catch (e) {
+ LOG("Blocklist::_loadBlocklistFromFile: Failed to load XML file " + e);
+ } finally {
+ if (cstream)
+ cstream.close();
+ if (fstream)
+ fstream.close();
+ }
+
+ if (text)
+ this._loadBlocklistFromString(text);
+ },
+
+ _isBlocklistLoaded: function() {
+ return this._addonEntries != null && this._gfxEntries != null && this._pluginEntries != null;
+ },
+
+ _isBlocklistPreloaded: function() {
+ return this._preloadedBlocklistContent != null;
+ },
+
+ /* Used for testing */
+ _clear: function() {
+ this._addonEntries = null;
+ this._gfxEntries = null;
+ this._pluginEntries = null;
+ this._preloadedBlocklistContent = null;
+ },
+
+ _preloadBlocklist: Task.async(function*() {
+ let profPath = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
+ try {
+ yield this._preloadBlocklistFile(profPath);
+ return;
+ } catch (e) {
+ LOG("Blocklist::_preloadBlocklist: Failed to load XML file " + e)
+ }
+
+ var appFile = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
+ try {
+ yield this._preloadBlocklistFile(appFile.path);
+ return;
+ } catch (e) {
+ LOG("Blocklist::_preloadBlocklist: Failed to load XML file " + e)
+ }
+
+ LOG("Blocklist::_preloadBlocklist: no XML File found");
+ }),
+
+ _preloadBlocklistFile: Task.async(function*(path) {
+ if (this._addonEntries) {
+ // The file has been already loaded.
+ return;
+ }
+
+ if (!gBlocklistEnabled) {
+ LOG("Blocklist::_preloadBlocklistFile: blocklist is disabled");
+ return;
+ }
+
+ let text = yield OS.File.read(path, { encoding: "utf-8" });
+
+ if (!this._addonEntries) {
+ // Store the content only if a sync load has not been performed in the meantime.
+ this._preloadedBlocklistContent = text;
+ }
+ }),
+
+ _loadBlocklistFromString : function(text) {
+ try {
+ var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+ var doc = parser.parseFromString(text, "text/xml");
+ if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) {
+ LOG("Blocklist::_loadBlocklistFromFile: aborting due to incorrect " +
+ "XML Namespace.\r\nExpected: " + XMLURI_BLOCKLIST + "\r\n" +
+ "Received: " + doc.documentElement.namespaceURI);
+ return;
+ }
+
+ var populateCertBlocklist = getPref("getBoolPref", PREF_ONECRL_VIA_AMO, true);
+
+ var childNodes = doc.documentElement.childNodes;
+ for (let element of childNodes) {
+ if (!(element instanceof Ci.nsIDOMElement))
+ continue;
+ switch (element.localName) {
+ case "emItems":
+ this._addonEntries = this._processItemNodes(element.childNodes, "emItem",
+ this._handleEmItemNode);
+ break;
+ case "pluginItems":
+ this._pluginEntries = this._processItemNodes(element.childNodes, "pluginItem",
+ this._handlePluginItemNode);
+ break;
+ case "certItems":
+ if (populateCertBlocklist) {
+ this._processItemNodes(element.childNodes, "certItem",
+ this._handleCertItemNode.bind(this));
+ }
+ break;
+ case "gfxItems":
+ // Parse as simple list of objects.
+ this._gfxEntries = this._processItemNodes(element.childNodes, "gfxBlacklistEntry",
+ this._handleGfxBlacklistNode);
+ break;
+ default:
+ LOG("Blocklist::_loadBlocklistFromString: ignored entries " + element.localName);
+ }
+ }
+ if (populateCertBlocklist) {
+ gCertBlocklistService.saveEntries();
+ }
+ if (this._gfxEntries.length > 0) {
+ this._notifyObserversBlocklistGFX();
+ }
+ }
+ catch (e) {
+ LOG("Blocklist::_loadBlocklistFromFile: Error constructing blocklist " + e);
+ return;
+ }
+ },
+
+ _processItemNodes: function(itemNodes, itemName, handler) {
+ var result = [];
+ for (var i = 0; i < itemNodes.length; ++i) {
+ var blocklistElement = itemNodes.item(i);
+ if (!(blocklistElement instanceof Ci.nsIDOMElement) ||
+ blocklistElement.localName != itemName)
+ continue;
+
+ handler(blocklistElement, result);
+ }
+ return result;
+ },
+
+ _handleCertItemNode: function(blocklistElement, result) {
+ let issuer = blocklistElement.getAttribute("issuerName");
+ if (issuer) {
+ for (let snElement of blocklistElement.children) {
+ try {
+ gCertBlocklistService.revokeCertByIssuerAndSerial(issuer, snElement.textContent);
+ } catch (e) {
+ // we want to keep trying other elements since missing all items
+ // is worse than missing one
+ LOG("Blocklist::_handleCertItemNode: Error adding revoked cert by Issuer and Serial" + e);
+ }
+ }
+ return;
+ }
+
+ let pubKeyHash = blocklistElement.getAttribute("pubKeyHash");
+ let subject = blocklistElement.getAttribute("subject");
+
+ if (pubKeyHash && subject) {
+ try {
+ gCertBlocklistService.revokeCertBySubjectAndPubKey(subject, pubKeyHash);
+ } catch (e) {
+ LOG("Blocklist::_handleCertItemNode: Error adding revoked cert by Subject and PubKey" + e);
+ }
+ }
+ },
+
+ _handleEmItemNode: function(blocklistElement, result) {
+ if (!matchesOSABI(blocklistElement))
+ return;
+
+ let blockEntry = {
+ versions: [],
+ prefs: [],
+ blockID: null,
+ attributes: new Map()
+ // Atleast one of EXTENSION_BLOCK_FILTERS must get added to attributes
+ };
+
+ // Any filter starting with '/' is interpreted as a regex. So if an attribute
+ // starts with a '/' it must be checked via a regex.
+ function regExpCheck(attr) {
+ return attr.startsWith("/") ? parseRegExp(attr) : attr;
+ }
+
+ for (let filter of EXTENSION_BLOCK_FILTERS) {
+ let attr = blocklistElement.getAttribute(filter);
+ if (attr)
+ blockEntry.attributes.set(filter, regExpCheck(attr));
+ }
+
+ var childNodes = blocklistElement.childNodes;
+
+ for (let x = 0; x < childNodes.length; x++) {
+ var childElement = childNodes.item(x);
+ if (!(childElement instanceof Ci.nsIDOMElement))
+ continue;
+ if (childElement.localName === "prefs") {
+ let prefElements = childElement.childNodes;
+ for (let i = 0; i < prefElements.length; i++) {
+ let prefElement = prefElements.item(i);
+ if (!(prefElement instanceof Ci.nsIDOMElement) ||
+ prefElement.localName !== "pref")
+ continue;
+ blockEntry.prefs.push(prefElement.textContent);
+ }
+ }
+ else if (childElement.localName === "versionRange")
+ blockEntry.versions.push(new BlocklistItemData(childElement));
+ }
+ // if only the extension ID is specified block all versions of the
+ // extension for the current application.
+ if (blockEntry.versions.length == 0)
+ blockEntry.versions.push(new BlocklistItemData(null));
+
+ blockEntry.blockID = blocklistElement.getAttribute("blockID");
+
+ result.push(blockEntry);
+ },
+
+ _handlePluginItemNode: function(blocklistElement, result) {
+ if (!matchesOSABI(blocklistElement))
+ return;
+
+ var matchNodes = blocklistElement.childNodes;
+ var blockEntry = {
+ matches: {},
+ versions: [],
+ blockID: null,
+ infoURL: null,
+ };
+ var hasMatch = false;
+ for (var x = 0; x < matchNodes.length; ++x) {
+ var matchElement = matchNodes.item(x);
+ if (!(matchElement instanceof Ci.nsIDOMElement))
+ continue;
+ if (matchElement.localName == "match") {
+ var name = matchElement.getAttribute("name");
+ var exp = matchElement.getAttribute("exp");
+ try {
+ blockEntry.matches[name] = new RegExp(exp, "m");
+ hasMatch = true;
+ } catch (e) {
+ // Ignore invalid regular expressions
+ }
+ }
+ if (matchElement.localName == "versionRange") {
+ blockEntry.versions.push(new BlocklistItemData(matchElement));
+ }
+ else if (matchElement.localName == "infoURL") {
+ blockEntry.infoURL = matchElement.textContent;
+ }
+ }
+ // Plugin entries require *something* to match to an actual plugin
+ if (!hasMatch)
+ return;
+ // Add a default versionRange if there wasn't one specified
+ if (blockEntry.versions.length == 0)
+ blockEntry.versions.push(new BlocklistItemData(null));
+
+ blockEntry.blockID = blocklistElement.getAttribute("blockID");
+
+ result.push(blockEntry);
+ },
+
+ // <gfxBlacklistEntry blockID="g60">
+ // <os>WINNT 6.0</os>
+ // <osversion>14</osversion> currently only used for Android
+ // <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ // <vendor>0x8086</vendor>
+ // <devices>
+ // <device>0x2582</device>
+ // <device>0x2782</device>
+ // </devices>
+ // <feature> DIRECT3D_10_LAYERS </feature>
+ // <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ // <driverVersion> 8.52.322.2202 </driverVersion>
+ // <driverVersionMax> 8.52.322.2202 </driverVersionMax>
+ // <driverVersionComparator> LESS_THAN_OR_EQUAL </driverVersionComparator>
+ // <model>foo</model>
+ // <product>foo</product>
+ // <manufacturer>foo</manufacturer>
+ // <hardware>foo</hardware>
+ // </gfxBlacklistEntry>
+ _handleGfxBlacklistNode: function (blocklistElement, result) {
+ const blockEntry = {};
+
+ // The blockID attribute is always present in the actual data produced on server
+ // (see https://github.com/mozilla/addons-server/blob/2016.05.05/src/olympia/blocklist/templates/blocklist/blocklist.xml#L74)
+ // But it is sometimes missing in test fixtures.
+ if (blocklistElement.hasAttribute("blockID")) {
+ blockEntry.blockID = blocklistElement.getAttribute("blockID");
+ }
+
+ // Trim helper (spaces, tabs, no-break spaces..)
+ const trim = (s) => (s || '').replace(/(^[\s\uFEFF\xA0]+)|([\s\uFEFF\xA0]+$)/g, "");
+
+ for (let i = 0; i < blocklistElement.childNodes.length; ++i) {
+ var matchElement = blocklistElement.childNodes.item(i);
+ if (!(matchElement instanceof Ci.nsIDOMElement))
+ continue;
+
+ let value;
+ if (matchElement.localName == "devices") {
+ value = [];
+ for (let j = 0; j < matchElement.childNodes.length; j++) {
+ const childElement = matchElement.childNodes.item(j);
+ const childValue = trim(childElement.textContent);
+ // Make sure no empty value is added.
+ if (childValue) {
+ if (/,/.test(childValue)) {
+ // Devices can't contain comma.
+ // (c.f serialization in _notifyObserversBlocklistGFX)
+ const e = new Error(`Unsupported device name ${childValue}`);
+ Components.utils.reportError(e);
+ }
+ else {
+ value.push(childValue);
+ }
+ }
+ }
+ } else if (matchElement.localName == "versionRange") {
+ value = {minVersion: trim(matchElement.getAttribute("minVersion")) || "0",
+ maxVersion: trim(matchElement.getAttribute("maxVersion")) || "*"};
+ } else {
+ value = trim(matchElement.textContent);
+ }
+ if (value) {
+ blockEntry[matchElement.localName] = value;
+ }
+ }
+ result.push(blockEntry);
+ },
+
+ /* See nsIBlocklistService */
+ getPluginBlocklistState: function(plugin, appVersion, toolkitVersion) {
+#ifdef MOZ_WIDGET_ANDROID
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+#endif
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+ return this._getPluginBlocklistState(plugin, this._pluginEntries,
+ appVersion, toolkitVersion);
+ },
+
+ /**
+ * Private helper to get the blocklist entry for a plugin given a set of
+ * blocklist entries and versions.
+ *
+ * @param plugin
+ * The nsIPluginTag to get the blocklist state for.
+ * @param pluginEntries
+ * The plugin blocklist entries to compare against.
+ * @param appVersion
+ * The application version to compare to, will use the current
+ * version if null.
+ * @param toolkitVersion
+ * The toolkit version to compare to, will use the current version if
+ * null.
+ * @returns {entry: blocklistEntry, version: blocklistEntryVersion},
+ * or null if there is no matching entry.
+ */
+ _getPluginBlocklistEntry: function(plugin, pluginEntries, appVersion, toolkitVersion) {
+ if (!gBlocklistEnabled)
+ return null;
+
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (!appVersion && !gApp.version)
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+
+ if (!appVersion)
+ appVersion = gApp.version;
+ if (!toolkitVersion)
+ toolkitVersion = gApp.platformVersion;
+
+ for (var blockEntry of pluginEntries) {
+ var matchFailed = false;
+ for (var name in blockEntry.matches) {
+ if (!(name in plugin) ||
+ typeof(plugin[name]) != "string" ||
+ !blockEntry.matches[name].test(plugin[name])) {
+ matchFailed = true;
+ break;
+ }
+ }
+
+ if (matchFailed)
+ continue;
+
+ for (let blockEntryVersion of blockEntry.versions) {
+ if (blockEntryVersion.includesItem(plugin.version, appVersion,
+ toolkitVersion)) {
+ return {entry: blockEntry, version: blockEntryVersion};
+ }
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Private version of getPluginBlocklistState that allows the caller to pass in
+ * the plugin blocklist entries.
+ *
+ * @param plugin
+ * The nsIPluginTag to get the blocklist state for.
+ * @param pluginEntries
+ * The plugin blocklist entries to compare against.
+ * @param appVersion
+ * The application version to compare to, will use the current
+ * version if null.
+ * @param toolkitVersion
+ * The toolkit version to compare to, will use the current version if
+ * null.
+ * @returns The blocklist state for the item, one of the STATE constants as
+ * defined in nsIBlocklistService.
+ */
+ _getPluginBlocklistState: function(plugin, pluginEntries, appVersion, toolkitVersion) {
+
+ let r = this._getPluginBlocklistEntry(plugin, pluginEntries,
+ appVersion, toolkitVersion);
+ if (!r) {
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ }
+
+ let {entry: blockEntry, version: blockEntryVersion} = r;
+
+ if (blockEntryVersion.severity >= gBlocklistLevel)
+ return Ci.nsIBlocklistService.STATE_BLOCKED;
+ if (blockEntryVersion.severity == SEVERITY_OUTDATED) {
+ let vulnerabilityStatus = blockEntryVersion.vulnerabilityStatus;
+ if (vulnerabilityStatus == VULNERABILITYSTATUS_UPDATE_AVAILABLE)
+ return Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE;
+ if (vulnerabilityStatus == VULNERABILITYSTATUS_NO_UPDATE)
+ return Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE;
+ return Ci.nsIBlocklistService.STATE_OUTDATED;
+ }
+ return Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
+ },
+
+ /* See nsIBlocklistService */
+ getPluginBlocklistURL: function(plugin) {
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+
+ let r = this._getPluginBlocklistEntry(plugin, this._pluginEntries);
+ if (!r) {
+ return null;
+ }
+ let {entry: blockEntry, version: blockEntryVersion} = r;
+ if (!blockEntry.blockID) {
+ return null;
+ }
+
+ return this._createBlocklistURL(blockEntry.blockID);
+ },
+
+ /* See nsIBlocklistService */
+ getPluginInfoURL: function(plugin) {
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+
+ let r = this._getPluginBlocklistEntry(plugin, this._pluginEntries);
+ if (!r) {
+ return null;
+ }
+ let {entry: blockEntry, version: blockEntryVersion} = r;
+ if (!blockEntry.blockID) {
+ return null;
+ }
+
+ return blockEntry.infoURL;
+ },
+
+ _notifyObserversBlocklistGFX: function () {
+ // Notify `GfxInfoBase`, by passing a string serialization.
+ // This way we avoid spreading XML structure logics there.
+ const payload = this._gfxEntries.map((r) => {
+ return Object.keys(r).sort().filter((k) => !/id|last_modified/.test(k)).map((key) => {
+ let value = r[key];
+ if (Array.isArray(value)) {
+ value = value.join(",");
+ } else if (value.hasOwnProperty("minVersion")) {
+ // When XML is parsed, both minVersion and maxVersion are set.
+ value = `${value.minVersion},${value.maxVersion}`;
+ }
+ return `${key}:${value}`;
+ }).join("\t");
+ }).join("\n");
+ Services.obs.notifyObservers(null, "blocklist-data-gfxItems", payload);
+ },
+
+ _notifyObserversBlocklistUpdated: function() {
+ Services.obs.notifyObservers(this, "blocklist-updated", "");
+ Services.ppmm.broadcastAsyncMessage("Blocklist:blocklistInvalidated", {});
+ },
+
+ _blocklistUpdated: function(oldAddonEntries, oldPluginEntries) {
+ var addonList = [];
+
+ // A helper function that reverts the prefs passed to default values.
+ function resetPrefs(prefs) {
+ for (let pref of prefs)
+ gPref.clearUserPref(pref);
+ }
+ const types = ["extension", "theme", "locale", "dictionary", "service"];
+ AddonManager.getAddonsByTypes(types, addons => {
+ for (let addon of addons) {
+ let oldState = Ci.nsIBlocklistService.STATE_NOTBLOCKED;
+ if (oldAddonEntries)
+ oldState = this._getAddonBlocklistState(addon, oldAddonEntries);
+ let state = this.getAddonBlocklistState(addon);
+
+ LOG("Blocklist state for " + addon.id + " changed from " +
+ oldState + " to " + state);
+
+ // We don't want to re-warn about add-ons
+ if (state == oldState)
+ continue;
+
+ if (state === Ci.nsIBlocklistService.STATE_BLOCKED) {
+ // It's a hard block. We must reset certain preferences.
+ let prefs = this._getAddonPrefs(addon);
+ resetPrefs(prefs);
+ }
+
+ // Ensure that softDisabled is false if the add-on is not soft blocked
+ if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
+ addon.softDisabled = false;
+
+ // Don't warn about add-ons becoming unblocked.
+ if (state == Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
+ continue;
+
+ // If an add-on has dropped from hard to soft blocked just mark it as
+ // soft disabled and don't warn about it.
+ if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
+ oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ addon.softDisabled = true;
+ continue;
+ }
+
+ // If the add-on is already disabled for some reason then don't warn
+ // about it
+ if (!addon.isActive) {
+ // But mark it as softblocked if necessary. Note that we avoid setting
+ // softDisabled at the same time as userDisabled to make it clear
+ // which was the original cause of the add-on becoming disabled in a
+ // way that the user can change.
+ if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED && !addon.userDisabled)
+ addon.softDisabled = true;
+ continue;
+ }
+
+ addonList.push({
+ name: addon.name,
+ version: addon.version,
+ icon: addon.iconURL,
+ disable: false,
+ blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
+ item: addon,
+ url: this.getAddonBlocklistURL(addon),
+ });
+ }
+
+ AddonManagerPrivate.updateAddonAppDisabledStates();
+
+ var phs = Cc["@mozilla.org/plugin/host;1"].
+ getService(Ci.nsIPluginHost);
+ var plugins = phs.getPluginTags();
+
+ for (let plugin of plugins) {
+ let oldState = -1;
+ if (oldPluginEntries)
+ oldState = this._getPluginBlocklistState(plugin, oldPluginEntries);
+ let state = this.getPluginBlocklistState(plugin);
+ LOG("Blocklist state for " + plugin.name + " changed from " +
+ oldState + " to " + state);
+ // We don't want to re-warn about items
+ if (state == oldState)
+ continue;
+
+ if (oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
+ plugin.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
+ }
+ else if (!plugin.disabled && state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
+ if (state != Ci.nsIBlocklistService.STATE_OUTDATED &&
+ state != Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE &&
+ state != Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE) {
+ addonList.push({
+ name: plugin.name,
+ version: plugin.version,
+ icon: "chrome://mozapps/skin/plugins/pluginGeneric.png",
+ disable: false,
+ blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
+ item: plugin,
+ url: this.getPluginBlocklistURL(plugin),
+ });
+ }
+ }
+ }
+
+ if (addonList.length == 0) {
+ this._notifyObserversBlocklistUpdated();
+ return;
+ }
+
+ if ("@mozilla.org/addons/blocklist-prompt;1" in Cc) {
+ try {
+ let blockedPrompter = Cc["@mozilla.org/addons/blocklist-prompt;1"]
+ .getService(Ci.nsIBlocklistPrompt);
+ blockedPrompter.prompt(addonList);
+ } catch (e) {
+ LOG(e);
+ }
+ this._notifyObserversBlocklistUpdated();
+ return;
+ }
+
+ var args = {
+ restart: false,
+ list: addonList
+ };
+ // This lets the dialog get the raw js object
+ args.wrappedJSObject = args;
+
+ /*
+ Some tests run without UI, so the async code listens to a message
+ that can be sent programatically
+ */
+ let applyBlocklistChanges = () => {
+ for (let addon of addonList) {
+ if (!addon.disable)
+ continue;
+
+ if (addon.item instanceof Ci.nsIPluginTag)
+ addon.item.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
+ else {
+ // This add-on is softblocked.
+ addon.item.softDisabled = true;
+ // We must revert certain prefs.
+ let prefs = this._getAddonPrefs(addon.item);
+ resetPrefs(prefs);
+ }
+ }
+
+ if (args.restart)
+ restartApp();
+
+ this._notifyObserversBlocklistUpdated();
+ Services.obs.removeObserver(applyBlocklistChanges, "addon-blocklist-closed");
+ }
+
+ Services.obs.addObserver(applyBlocklistChanges, "addon-blocklist-closed", false);
+
+ if (getPref("getBoolPref", PREF_BLOCKLIST_SUPPRESSUI, false)) {
+ applyBlocklistChanges();
+ return;
+ }
+
+ function blocklistUnloadHandler(event) {
+ if (event.target.location == URI_BLOCKLIST_DIALOG) {
+ applyBlocklistChanges();
+ blocklistWindow.removeEventListener("unload", blocklistUnloadHandler);
+ }
+ }
+
+ let blocklistWindow = Services.ww.openWindow(null, URI_BLOCKLIST_DIALOG, "",
+ "chrome,centerscreen,dialog,titlebar", args);
+ if (blocklistWindow)
+ blocklistWindow.addEventListener("unload", blocklistUnloadHandler, false);
+ });
+ },
+
+ classID: Components.ID("{66354bc9-7ed1-4692-ae1d-8da97d6b205e}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIBlocklistService,
+ Ci.nsITimerCallback]),
+};
+
+/**
+ * Helper for constructing a blocklist.
+ */
+function BlocklistItemData(versionRangeElement) {
+ var versionRange = this.getBlocklistVersionRange(versionRangeElement);
+ this.minVersion = versionRange.minVersion;
+ this.maxVersion = versionRange.maxVersion;
+ if (versionRangeElement && versionRangeElement.hasAttribute("severity"))
+ this.severity = versionRangeElement.getAttribute("severity");
+ else
+ this.severity = DEFAULT_SEVERITY;
+ if (versionRangeElement && versionRangeElement.hasAttribute("vulnerabilitystatus")) {
+ this.vulnerabilityStatus = versionRangeElement.getAttribute("vulnerabilitystatus");
+ } else {
+ this.vulnerabilityStatus = VULNERABILITYSTATUS_NONE;
+ }
+ this.targetApps = { };
+ var found = false;
+
+ if (versionRangeElement) {
+ for (var i = 0; i < versionRangeElement.childNodes.length; ++i) {
+ var targetAppElement = versionRangeElement.childNodes.item(i);
+ if (!(targetAppElement instanceof Ci.nsIDOMElement) ||
+ targetAppElement.localName != "targetApplication")
+ continue;
+ found = true;
+ // default to the current application if id is not provided.
+ var appID = targetAppElement.hasAttribute("id") ? targetAppElement.getAttribute("id") : gApp.ID;
+ this.targetApps[appID] = this.getBlocklistAppVersions(targetAppElement);
+ }
+ }
+ // Default to all versions of the current application when no targetApplication
+ // elements were found
+ if (!found)
+ this.targetApps[gApp.ID] = this.getBlocklistAppVersions(null);
+}
+
+BlocklistItemData.prototype = {
+ /**
+ * Tests if a version of an item is included in the version range and target
+ * application information represented by this BlocklistItemData using the
+ * provided application and toolkit versions.
+ * @param version
+ * The version of the item being tested.
+ * @param appVersion
+ * The application version to test with.
+ * @param toolkitVersion
+ * The toolkit version to test with.
+ * @returns True if the version range covers the item version and application
+ * or toolkit version.
+ */
+ includesItem: function(version, appVersion, toolkitVersion) {
+ // Some platforms have no version for plugins, these don't match if there
+ // was a min/maxVersion provided
+ if (!version && (this.minVersion || this.maxVersion))
+ return false;
+
+ // Check if the item version matches
+ if (!this.matchesRange(version, this.minVersion, this.maxVersion))
+ return false;
+
+ // Check if the application version matches
+ if (this.matchesTargetRange(gApp.ID, appVersion))
+ return true;
+
+ // Check if the toolkit version matches
+ return this.matchesTargetRange(TOOLKIT_ID, toolkitVersion);
+ },
+
+ /**
+ * Checks if a version is higher than or equal to the minVersion (if provided)
+ * and lower than or equal to the maxVersion (if provided).
+ * @param version
+ * The version to test.
+ * @param minVersion
+ * The minimum version. If null it is assumed that version is always
+ * larger.
+ * @param maxVersion
+ * The maximum version. If null it is assumed that version is always
+ * smaller.
+ */
+ matchesRange: function(version, minVersion, maxVersion) {
+ if (minVersion && gVersionChecker.compare(version, minVersion) < 0)
+ return false;
+ if (maxVersion && gVersionChecker.compare(version, maxVersion) > 0)
+ return false;
+ return true;
+ },
+
+ /**
+ * Tests if there is a matching range for the given target application id and
+ * version.
+ * @param appID
+ * The application ID to test for, may be for an application or toolkit
+ * @param appVersion
+ * The version of the application to test for.
+ * @returns True if this version range covers the application version given.
+ */
+ matchesTargetRange: function(appID, appVersion) {
+ var blTargetApp = this.targetApps[appID];
+ if (!blTargetApp)
+ return false;
+
+ for (let app of blTargetApp) {
+ if (this.matchesRange(appVersion, app.minVersion, app.maxVersion))
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Retrieves a version range (e.g. minVersion and maxVersion) for a
+ * blocklist item's targetApplication element.
+ * @param targetAppElement
+ * A targetApplication blocklist element.
+ * @returns An array of JS objects with the following properties:
+ * "minVersion" The minimum version in a version range (default = null).
+ * "maxVersion" The maximum version in a version range (default = null).
+ */
+ getBlocklistAppVersions: function(targetAppElement) {
+ var appVersions = [ ];
+
+ if (targetAppElement) {
+ for (var i = 0; i < targetAppElement.childNodes.length; ++i) {
+ var versionRangeElement = targetAppElement.childNodes.item(i);
+ if (!(versionRangeElement instanceof Ci.nsIDOMElement) ||
+ versionRangeElement.localName != "versionRange")
+ continue;
+ appVersions.push(this.getBlocklistVersionRange(versionRangeElement));
+ }
+ }
+ // return minVersion = null and maxVersion = null if no specific versionRange
+ // elements were found
+ if (appVersions.length == 0)
+ appVersions.push(this.getBlocklistVersionRange(null));
+ return appVersions;
+ },
+
+ /**
+ * Retrieves a version range (e.g. minVersion and maxVersion) for a blocklist
+ * versionRange element.
+ * @param versionRangeElement
+ * The versionRange blocklist element.
+ * @returns A JS object with the following properties:
+ * "minVersion" The minimum version in a version range (default = null).
+ * "maxVersion" The maximum version in a version range (default = null).
+ */
+ getBlocklistVersionRange: function(versionRangeElement) {
+ var minVersion = null;
+ var maxVersion = null;
+ if (!versionRangeElement)
+ return { minVersion: minVersion, maxVersion: maxVersion };
+
+ if (versionRangeElement.hasAttribute("minVersion"))
+ minVersion = versionRangeElement.getAttribute("minVersion");
+ if (versionRangeElement.hasAttribute("maxVersion"))
+ maxVersion = versionRangeElement.getAttribute("maxVersion");
+
+ return { minVersion: minVersion, maxVersion: maxVersion };
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Blocklist]);
diff --git a/toolkit/components/blocklist/nsBlocklistServiceContent.js b/toolkit/components/blocklist/nsBlocklistServiceContent.js
new file mode 100644
index 000000000..1752924b5
--- /dev/null
+++ b/toolkit/components/blocklist/nsBlocklistServiceContent.js
@@ -0,0 +1,113 @@
+/* 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const kMissingAPIMessage = "Unsupported blocklist call in the child process."
+
+/*
+ * A lightweight blocklist proxy for the content process that traps plugin
+ * related blocklist checks and forwards them to the parent. This interface is
+ * primarily designed to insure overlays work.. it does not control plugin
+ * or addon loading.
+ */
+
+function Blocklist() {
+ this.init();
+}
+
+Blocklist.prototype = {
+ classID: Components.ID("{e0a106ed-6ad4-47a4-b6af-2f1c8aa4712d}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIBlocklistService]),
+
+ init: function() {
+ Services.cpmm.addMessageListener("Blocklist:blocklistInvalidated", this);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ },
+
+ uninit: function() {
+ Services.cpmm.removeMessageListener("Blocklist:blocklistInvalidated", this);
+ Services.obs.removeObserver(this, "xpcom-shutdown", false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "xpcom-shutdown":
+ this.uninit();
+ break;
+ }
+ },
+
+ // Message manager message handlers
+ receiveMessage: function(aMsg) {
+ switch (aMsg.name) {
+ case "Blocklist:blocklistInvalidated":
+ Services.obs.notifyObservers(null, "blocklist-updated", null);
+ Services.cpmm.sendAsyncMessage("Blocklist:content-blocklist-updated");
+ break;
+ default:
+ throw new Error("Unknown blocklist message received from content: " + aMsg.name);
+ }
+ },
+
+ /*
+ * A helper that queries key data from a plugin or addon object
+ * and generates a simple data wrapper suitable for ipc. We hand
+ * these directly to the nsBlockListService in the parent which
+ * doesn't query for much.. allowing us to get away with this.
+ */
+ flattenObject: function(aTag) {
+ // Based on debugging the nsBlocklistService, these are the props the
+ // parent side will check on our objects.
+ let props = ["name", "description", "filename", "version"];
+ let dataWrapper = {};
+ for (let prop of props) {
+ dataWrapper[prop] = aTag[prop];
+ }
+ return dataWrapper;
+ },
+
+ // We support the addon methods here for completeness, but content currently
+ // only calls getPluginBlocklistState.
+
+ isAddonBlocklisted: function(aAddon, aAppVersion, aToolkitVersion) {
+ return true;
+ },
+
+ getAddonBlocklistState: function(aAddon, aAppVersion, aToolkitVersion) {
+ return Components.interfaces.nsIBlocklistService.STATE_BLOCKED;
+ },
+
+ // There are a few callers in layout that rely on this.
+ getPluginBlocklistState: function(aPluginTag, aAppVersion, aToolkitVersion) {
+ return Services.cpmm.sendSyncMessage("Blocklist:getPluginBlocklistState", {
+ addonData: this.flattenObject(aPluginTag),
+ appVersion: aAppVersion,
+ toolkitVersion: aToolkitVersion
+ })[0];
+ },
+
+ getAddonBlocklistURL: function(aAddon, aAppVersion, aToolkitVersion) {
+ throw new Error(kMissingAPIMessage);
+ },
+
+ getPluginBlocklistURL: function(aPluginTag) {
+ throw new Error(kMissingAPIMessage);
+ },
+
+ getPluginInfoURL: function(aPluginTag) {
+ throw new Error(kMissingAPIMessage);
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Blocklist]);
diff --git a/toolkit/components/build/nsToolkitCompsModule.cpp b/toolkit/components/build/nsToolkitCompsModule.cpp
index f81e35f23..190c4da06 100644
--- a/toolkit/components/build/nsToolkitCompsModule.cpp
+++ b/toolkit/components/build/nsToolkitCompsModule.cpp
@@ -48,7 +48,7 @@
#include "NativeFileWatcherNotSupported.h"
#endif // (XP_WIN)
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
#define MOZ_HAS_TERMINATOR
#endif
diff --git a/toolkit/components/captivedetect/captivedetect.js b/toolkit/components/captivedetect/captivedetect.js
index 5493ecec6..0e8c75981 100644
--- a/toolkit/components/captivedetect/captivedetect.js
+++ b/toolkit/components/captivedetect/captivedetect.js
@@ -90,7 +90,7 @@ function LoginObserver(captivePortalDetector) {
.getService(Ci.nsIHttpActivityDistributor);
let urlFetcher = null;
- let waitForNetworkActivity = Services.appinfo.widgetToolkit == "gonk";
+ let waitForNetworkActivity = false;
let pageCheckingDone = function pageCheckingDone() {
if (state === LOGIN_OBSERVER_STATE_VERIFYING) {
diff --git a/toolkit/components/diskspacewatcher/DiskSpaceWatcher.cpp b/toolkit/components/diskspacewatcher/DiskSpaceWatcher.cpp
index 950d3b487..7f3b8cd08 100644
--- a/toolkit/components/diskspacewatcher/DiskSpaceWatcher.cpp
+++ b/toolkit/components/diskspacewatcher/DiskSpaceWatcher.cpp
@@ -145,9 +145,6 @@ static const mozilla::Module::ContractIDEntry kDiskSpaceWatcherContracts[] = {
};
static const mozilla::Module::CategoryEntry kDiskSpaceWatcherCategories[] = {
-#ifdef MOZ_WIDGET_GONK
- { "profile-after-change", "Disk Space Watcher Service", DISKSPACEWATCHER_CONTRACTID },
-#endif
{ nullptr }
};
diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
index c24c5d631..305284749 100644
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -410,8 +410,6 @@ this.DownloadIntegration = {
directoryPath = yield this.getPreferredDownloadsDirectory();
#elifdef MOZ_WIDGET_ANDROID
directoryPath = yield this.getSystemDownloadsDirectory();
-#elifdef MOZ_WIDGET_GONK
- directoryPath = yield this.getSystemDownloadsDirectory();
#else
directoryPath = this._getDirectory("TmpD");
#endif
diff --git a/toolkit/components/jsdownloads/src/DownloadPlatform.cpp b/toolkit/components/jsdownloads/src/DownloadPlatform.cpp
index 1506b7c30..d91124ee6 100644
--- a/toolkit/components/jsdownloads/src/DownloadPlatform.cpp
+++ b/toolkit/components/jsdownloads/src/DownloadPlatform.cpp
@@ -102,7 +102,7 @@ nsresult DownloadPlatform::DownloadDone(nsIURI* aSource, nsIURI* aReferrer, nsIF
const nsACString& aContentType, bool aIsPrivate)
{
#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_ANDROID) \
- || defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_GONK)
+ || defined(MOZ_WIDGET_GTK)
nsAutoString path;
if (aTarget && NS_SUCCEEDED(aTarget->GetPath(path))) {
diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build
index c34c1f18d..c11f62792 100644
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -17,6 +17,7 @@ DIRS += [
'alerts',
'apppicker',
'asyncshutdown',
+ 'blocklist',
'commandlines',
'console',
'contentprefs',
diff --git a/toolkit/components/narrate/NarrateControls.jsm b/toolkit/components/narrate/NarrateControls.jsm
index 7d8794b18..be3ce636c 100644
--- a/toolkit/components/narrate/NarrateControls.jsm
+++ b/toolkit/components/narrate/NarrateControls.jsm
@@ -16,9 +16,9 @@ this.EXPORTED_SYMBOLS = ["NarrateControls"];
var gStrings = Services.strings.createBundle("chrome://global/locale/narrate.properties");
-function NarrateControls(mm, win) {
- this._mm = mm;
+function NarrateControls(win, languagePromise) {
this._winRef = Cu.getWeakReference(win);
+ this._languagePromise = languagePromise;
win.addEventListener("unload", this);
@@ -37,16 +37,12 @@ function NarrateControls(mm, win) {
}
let dropdown = win.document.createElement("ul");
- dropdown.className = "dropdown";
- dropdown.id = "narrate-dropdown";
+ dropdown.className = "dropdown narrate-dropdown";
// We need inline svg here for the animation to work (bug 908634 & 1190881).
- // The style animation can't be scoped (bug 830056).
+ // eslint-disable-next-line no-unsanitized/property
dropdown.innerHTML =
- localize`<style scoped>
- @import url("chrome://global/skin/narrateControls.css");
- </style>
- <li>
- <button class="dropdown-toggle button" id="narrate-toggle"
+ localize`<li>
+ <button class="dropdown-toggle button narrate-toggle"
title="${"narrate"}" hidden>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
@@ -60,11 +56,11 @@ function NarrateControls(mm, win) {
100% { transform: scaleY(1); }
}
- #waveform > rect {
+ .waveform > rect {
fill: #808080;
}
- .speaking #waveform > rect {
+ .speaking .waveform > rect {
fill: #58bf43;
transform-box: fill-box;
transform-origin: 50% 50%;
@@ -74,15 +70,15 @@ function NarrateControls(mm, win) {
animation-timing-function: linear;
}
- #waveform > rect:nth-child(2) { animation-delay: 250ms; }
- #waveform > rect:nth-child(3) { animation-delay: 500ms; }
- #waveform > rect:nth-child(4) { animation-delay: 750ms; }
- #waveform > rect:nth-child(5) { animation-delay: 1000ms; }
- #waveform > rect:nth-child(6) { animation-delay: 1250ms; }
- #waveform > rect:nth-child(7) { animation-delay: 1500ms; }
+ .waveform > rect:nth-child(2) { animation-delay: 250ms; }
+ .waveform > rect:nth-child(3) { animation-delay: 500ms; }
+ .waveform > rect:nth-child(4) { animation-delay: 750ms; }
+ .waveform > rect:nth-child(5) { animation-delay: 1000ms; }
+ .waveform > rect:nth-child(6) { animation-delay: 1250ms; }
+ .waveform > rect:nth-child(7) { animation-delay: 1500ms; }
</style>
- <g id="waveform">
+ <g class="waveform">
<rect x="1" y="8" width="2" height="8" rx=".5" ry=".5" />
<rect x="4" y="5" width="2" height="14" rx=".5" ry=".5" />
<rect x="7" y="8" width="2" height="8" rx=".5" ry=".5" />
@@ -95,35 +91,35 @@ function NarrateControls(mm, win) {
</button>
</li>
<li class="dropdown-popup">
- <div id="narrate-control" class="narrate-row">
- <button disabled id="narrate-skip-previous"
+ <div class="narrate-row narrate-control">
+ <button disabled class="narrate-skip-previous"
title="${"back"}"></button>
- <button id="narrate-start-stop" title="${"start"}"></button>
- <button disabled id="narrate-skip-next"
+ <button class="narrate-start-stop" title="${"start"}"></button>
+ <button disabled class="narrate-skip-next"
title="${"forward"}"></button>
</div>
- <div id="narrate-rate" class="narrate-row">
- <input id="narrate-rate-input" value="0" title="${"speed"}"
+ <div class="narrate-row narrate-rate">
+ <input class="narrate-rate-input" value="0" title="${"speed"}"
step="5" max="100" min="-100" type="range">
</div>
- <div id="narrate-voices" class="narrate-row"></div>
+ <div class="narrate-row narrate-voices"></div>
<div class="dropdown-arrow"></div>
</li>`;
- this.narrator = new Narrator(win);
+ this.narrator = new Narrator(win, languagePromise);
let branch = Services.prefs.getBranch("narrate.");
let selectLabel = gStrings.GetStringFromName("selectvoicelabel");
this.voiceSelect = new VoiceSelect(win, selectLabel);
this.voiceSelect.element.addEventListener("change", this);
- this.voiceSelect.element.id = "voice-select";
+ this.voiceSelect.element.classList.add("voice-select");
win.speechSynthesis.addEventListener("voiceschanged", this);
- dropdown.querySelector("#narrate-voices").appendChild(
+ dropdown.querySelector(".narrate-voices").appendChild(
this.voiceSelect.element);
dropdown.addEventListener("click", this, true);
- let rateRange = dropdown.querySelector("#narrate-rate > input");
+ let rateRange = dropdown.querySelector(".narrate-rate > input");
rateRange.addEventListener("change", this);
// The rate is stored as an integer.
@@ -131,15 +127,15 @@ function NarrateControls(mm, win) {
this._setupVoices();
- let tb = win.document.getElementById("reader-toolbar");
+ let tb = win.document.querySelector(".reader-toolbar");
tb.appendChild(dropdown);
}
NarrateControls.prototype = {
- handleEvent: function(evt) {
+ handleEvent(evt) {
switch (evt.type) {
case "change":
- if (evt.target.id == "narrate-rate-input") {
+ if (evt.target.classList.contains("narrate-rate-input")) {
this._onRateInput(evt);
} else {
this._onVoiceChange();
@@ -162,8 +158,8 @@ NarrateControls.prototype = {
/**
* Returns true if synth voices are available.
*/
- _setupVoices: function() {
- return this.narrator.languagePromise.then(language => {
+ _setupVoices() {
+ return this._languagePromise.then(language => {
this.voiceSelect.clear();
let win = this._win;
let voicePrefs = this._getVoicePref();
@@ -190,7 +186,7 @@ NarrateControls.prototype = {
this.voiceSelect.addOptions(options);
}
- let narrateToggle = win.document.getElementById("narrate-toggle");
+ let narrateToggle = win.document.querySelector(".narrate-toggle");
let histogram = Services.telemetry.getKeyedHistogramById(
"NARRATE_CONTENT_BY_LANGUAGE_2");
let initial = !this._voicesInitialized;
@@ -210,7 +206,7 @@ NarrateControls.prototype = {
});
},
- _getVoicePref: function() {
+ _getVoicePref() {
let voicePref = Services.prefs.getCharPref("narrate.voice");
try {
return JSON.parse(voicePref);
@@ -219,15 +215,15 @@ NarrateControls.prototype = {
}
},
- _onRateInput: function(evt) {
+ _onRateInput(evt) {
AsyncPrefs.set("narrate.rate", parseInt(evt.target.value, 10));
this.narrator.setRate(this._convertRate(evt.target.value));
},
- _onVoiceChange: function() {
+ _onVoiceChange() {
let voice = this.voice;
this.narrator.setVoice(voice);
- this.narrator.languagePromise.then(language => {
+ this._languagePromise.then(language => {
if (language) {
let voicePref = this._getVoicePref();
voicePref[language || "default"] = voice;
@@ -236,42 +232,39 @@ NarrateControls.prototype = {
});
},
- _onButtonClick: function(evt) {
- switch (evt.target.id) {
- case "narrate-skip-previous":
- this.narrator.skipPrevious();
- break;
- case "narrate-skip-next":
- this.narrator.skipNext();
- break;
- case "narrate-start-stop":
- if (this.narrator.speaking) {
- this.narrator.stop();
- } else {
- this._updateSpeechControls(true);
- let options = { rate: this.rate, voice: this.voice };
- this.narrator.start(options).then(() => {
- this._updateSpeechControls(false);
- }, err => {
- Cu.reportError(`Narrate failed: ${err}.`);
- this._updateSpeechControls(false);
- });
- }
- break;
+ _onButtonClick(evt) {
+ let classList = evt.target.classList;
+ if (classList.contains("narrate-skip-previous")) {
+ this.narrator.skipPrevious();
+ } else if (classList.contains("narrate-skip-next")) {
+ this.narrator.skipNext();
+ } else if (classList.contains("narrate-start-stop")) {
+ if (this.narrator.speaking) {
+ this.narrator.stop();
+ } else {
+ this._updateSpeechControls(true);
+ let options = { rate: this.rate, voice: this.voice };
+ this.narrator.start(options).then(() => {
+ this._updateSpeechControls(false);
+ }, err => {
+ Cu.reportError(`Narrate failed: ${err}.`);
+ this._updateSpeechControls(false);
+ });
+ }
}
},
- _updateSpeechControls: function(speaking) {
- let dropdown = this._doc.getElementById("narrate-dropdown");
+ _updateSpeechControls(speaking) {
+ let dropdown = this._doc.querySelector(".narrate-dropdown");
dropdown.classList.toggle("keep-open", speaking);
dropdown.classList.toggle("speaking", speaking);
- let startStopButton = this._doc.getElementById("narrate-start-stop");
+ let startStopButton = this._doc.querySelector(".narrate-start-stop");
startStopButton.title =
gStrings.GetStringFromName(speaking ? "stop" : "start");
- this._doc.getElementById("narrate-skip-previous").disabled = !speaking;
- this._doc.getElementById("narrate-skip-next").disabled = !speaking;
+ this._doc.querySelector(".narrate-skip-previous").disabled = !speaking;
+ this._doc.querySelector(".narrate-skip-next").disabled = !speaking;
if (speaking) {
TelemetryStopwatch.start("NARRATE_CONTENT_SPEAKTIME_MS", this);
@@ -280,7 +273,7 @@ NarrateControls.prototype = {
}
},
- _createVoiceLabel: function(voice) {
+ _createVoiceLabel(voice) {
// This is a highly imperfect method of making human-readable labels
// for system voices. Because each platform has a different naming scheme
// for voices, we use a different method for each platform.
@@ -303,7 +296,7 @@ NarrateControls.prototype = {
}
},
- _getLanguageName: function(lang) {
+ _getLanguageName(lang) {
if (!this._langStrings) {
this._langStrings = Services.strings.createBundle(
"chrome://global/locale/languageNames.properties ");
@@ -317,7 +310,7 @@ NarrateControls.prototype = {
}
},
- _convertRate: function(rate) {
+ _convertRate(rate) {
// We need to convert a relative percentage value to a fraction rate value.
// eg. -100 is half the speed, 100 is twice the speed in percentage,
// 0.5 is half the speed and 2 is twice the speed in fractions.
@@ -334,7 +327,7 @@ NarrateControls.prototype = {
get rate() {
return this._convertRate(
- this._doc.getElementById("narrate-rate-input").value);
+ this._doc.querySelector(".narrate-rate-input").value);
},
get voice() {
diff --git a/toolkit/components/narrate/Narrator.jsm b/toolkit/components/narrate/Narrator.jsm
index ade06510e..ac0b2e040 100644
--- a/toolkit/components/narrate/Narrator.jsm
+++ b/toolkit/components/narrate/Narrator.jsm
@@ -8,8 +8,6 @@ const { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
- "resource:///modules/translation/LanguageDetector.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
@@ -24,29 +22,13 @@ const kTextStylesRules = ["font-family", "font-kerning", "font-size",
"line-height", "letter-spacing", "text-orientation",
"text-transform", "word-spacing"];
-function Narrator(win) {
+function Narrator(win, languagePromise) {
this._winRef = Cu.getWeakReference(win);
+ this._languagePromise = languagePromise;
this._inTest = Services.prefs.getBoolPref("narrate.test");
this._speechOptions = {};
this._startTime = 0;
this._stopped = false;
-
- this.languagePromise = new Promise(resolve => {
- let detect = () => {
- win.document.removeEventListener("AboutReaderContentReady", detect);
- let sampleText = this._doc.getElementById(
- "moz-reader-content").textContent.substring(0, 60 * 1024);
- LanguageDetector.detectLanguage(sampleText).then(result => {
- resolve(result.confident ? result.language : null);
- });
- };
-
- if (win.document.body.classList.contains("loaded")) {
- detect();
- } else {
- win.document.addEventListener("AboutReaderContentReady", detect);
- }
- });
}
Narrator.prototype = {
@@ -71,7 +53,7 @@ Narrator.prototype = {
// For example, paragraphs. But nested anchors and other elements
// are not interesting since their text already appears in their
// parent's textContent.
- acceptNode: function(node) {
+ acceptNode(node) {
if (this._matches.has(node.parentNode)) {
// Reject sub-trees of accepted nodes.
return nf.FILTER_REJECT;
@@ -107,7 +89,7 @@ Narrator.prototype = {
// are no other strong references, and it will be GC'ed. Instead,
// we rely on the window's lifetime and use it as a weak reference.
this._treeWalkerRef.set(this._win,
- this._doc.createTreeWalker(this._doc.getElementById("container"),
+ this._doc.createTreeWalker(this._doc.querySelector(".container"),
nf.SHOW_ELEMENT, filter, false));
}
@@ -124,7 +106,7 @@ Narrator.prototype = {
this._win.speechSynthesis.pending;
},
- _getVoice: function(voiceURI) {
+ _getVoice(voiceURI) {
if (!this._voiceMap || !this._voiceMap.has(voiceURI)) {
this._voiceMap = new Map(
this._win.speechSynthesis.getVoices().map(v => [v.voiceURI, v]));
@@ -133,7 +115,7 @@ Narrator.prototype = {
return this._voiceMap.get(voiceURI);
},
- _isParagraphInView: function(paragraph) {
+ _isParagraphInView(paragraph) {
if (!paragraph) {
return false;
}
@@ -142,13 +124,13 @@ Narrator.prototype = {
return bb.top >= 0 && bb.top < this._win.innerHeight;
},
- _sendTestEvent: function(eventType, detail) {
+ _sendTestEvent(eventType, detail) {
let win = this._win;
win.dispatchEvent(new win.CustomEvent(eventType,
{ detail: Cu.cloneInto(detail, win.document) }));
},
- _speakInner: function() {
+ _speakInner() {
this._win.speechSynthesis.cancel();
let tw = this._treeWalker;
let paragraph = tw.currentNode;
@@ -238,18 +220,12 @@ Narrator.prototype = {
return;
}
- // Match non-whitespace. This isn't perfect, but the most universal
- // solution for now.
- let reWordBoundary = /\S+/g;
- // Match the first word from the boundary event offset.
- reWordBoundary.lastIndex = e.charIndex;
- let firstIndex = reWordBoundary.exec(paragraph.textContent);
- if (firstIndex) {
- highlighter.highlight(firstIndex.index, reWordBoundary.lastIndex);
+ if (e.charLength) {
+ highlighter.highlight(e.charIndex, e.charLength);
if (this._inTest) {
this._sendTestEvent("wordhighlight", {
- start: firstIndex.index,
- end: reWordBoundary.lastIndex
+ start: e.charIndex,
+ end: e.charIndex + e.charLength
});
}
}
@@ -259,14 +235,14 @@ Narrator.prototype = {
});
},
- start: function(speechOptions) {
+ start(speechOptions) {
this._speechOptions = {
rate: speechOptions.rate,
voice: this._getVoice(speechOptions.voice)
};
this._stopped = false;
- return this.languagePromise.then(language => {
+ return this._languagePromise.then(language => {
if (!this._speechOptions.voice) {
this._speechOptions.lang = language;
}
@@ -288,32 +264,32 @@ Narrator.prototype = {
});
},
- stop: function() {
+ stop() {
this._stopped = true;
this._win.speechSynthesis.cancel();
},
- skipNext: function() {
+ skipNext() {
this._win.speechSynthesis.cancel();
},
- skipPrevious: function() {
+ skipPrevious() {
this._goBackParagraphs(this._timeIntoParagraph < PREV_THRESHOLD ? 2 : 1);
},
- setRate: function(rate) {
+ setRate(rate) {
this._speechOptions.rate = rate;
/* repeat current paragraph */
this._goBackParagraphs(1);
},
- setVoice: function(voice) {
+ setVoice(voice) {
this._speechOptions.voice = this._getVoice(voice);
/* repeat current paragraph */
this._goBackParagraphs(1);
},
- _goBackParagraphs: function(count) {
+ _goBackParagraphs(count) {
let tw = this._treeWalker;
for (let i = 0; i < count; i++) {
if (!tw.previousNode()) {
@@ -338,13 +314,13 @@ Highlighter.prototype = {
* Highlight the range within offsets relative to the container.
*
* @param {Number} startOffset the start offset
- * @param {Number} endOffset the end offset
+ * @param {Number} length the length in characters of the range
*/
- highlight: function(startOffset, endOffset) {
+ highlight(startOffset, length) {
let containerRect = this.container.getBoundingClientRect();
- let range = this._getRange(startOffset, endOffset);
+ let range = this._getRange(startOffset, startOffset + length);
let rangeRects = range.getClientRects();
- let win = this.container.ownerDocument.defaultView;
+ let win = this.container.ownerGlobal;
let computedStyle = win.getComputedStyle(range.endContainer.parentNode);
let nodes = this._getFreshHighlightNodes(rangeRects.length);
@@ -386,7 +362,7 @@ Highlighter.prototype = {
/**
* Releases reference to container and removes all highlight nodes.
*/
- remove: function() {
+ remove() {
for (let node of this._nodes) {
node.remove();
}
@@ -400,7 +376,7 @@ Highlighter.prototype = {
*
* @param {Number} count number of nodes needed
*/
- _getFreshHighlightNodes: function(count) {
+ _getFreshHighlightNodes(count) {
let doc = this.container.ownerDocument;
let nodes = Array.from(this._nodes);
@@ -427,7 +403,7 @@ Highlighter.prototype = {
* @param {Number} startOffset the start offset
* @param {Number} endOffset the end offset
*/
- _getRange: function(startOffset, endOffset) {
+ _getRange(startOffset, endOffset) {
let doc = this.container.ownerDocument;
let i = 0;
let treeWalker = doc.createTreeWalker(
diff --git a/toolkit/components/narrate/VoiceSelect.jsm b/toolkit/components/narrate/VoiceSelect.jsm
index b283a06b3..861a21c97 100644
--- a/toolkit/components/narrate/VoiceSelect.jsm
+++ b/toolkit/components/narrate/VoiceSelect.jsm
@@ -13,6 +13,7 @@ function VoiceSelect(win, label) {
let element = win.document.createElement("div");
element.classList.add("voiceselect");
+ // eslint-disable-next-line no-unsanitized/property
element.innerHTML =
`<button class="select-toggle" aria-controls="voice-options">
<span class="label">${label}</span> <span class="current-voice"></span>
@@ -37,7 +38,7 @@ function VoiceSelect(win, label) {
}
VoiceSelect.prototype = {
- add: function(label, value) {
+ add(label, value) {
let option = this._doc.createElement("button");
option.dataset.value = value;
option.classList.add("option");
@@ -48,7 +49,7 @@ VoiceSelect.prototype = {
return option;
},
- addOptions: function(options) {
+ addOptions(options) {
let selected = null;
for (let option of options) {
if (option.selected) {
@@ -61,11 +62,11 @@ VoiceSelect.prototype = {
this._select(selected || this.options[0], true);
},
- clear: function() {
+ clear() {
this.listbox.innerHTML = "";
},
- toggleList: function(force, focus = true) {
+ toggleList(force, focus = true) {
if (this.element.classList.toggle("open", force)) {
if (focus) {
(this.selected || this.options[0]).focus();
@@ -84,7 +85,7 @@ VoiceSelect.prototype = {
}
},
- handleEvent: function(evt) {
+ handleEvent(evt) {
let target = evt.target;
switch (evt.type) {
@@ -131,7 +132,7 @@ VoiceSelect.prototype = {
}
},
- _getPagedOption: function(option, up) {
+ _getPagedOption(option, up) {
let height = elem => elem.getBoundingClientRect().height;
let listboxHeight = height(this.listbox);
@@ -148,7 +149,7 @@ VoiceSelect.prototype = {
return next;
},
- _keyPressedButton: function(evt) {
+ _keyPressedButton(evt) {
if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) {
this.toggleList(true);
return;
@@ -178,7 +179,7 @@ VoiceSelect.prototype = {
}
},
- _keyPressedInBox: function(evt) {
+ _keyPressedInBox(evt) {
let toFocus;
let cur = this._doc.activeElement;
@@ -212,7 +213,7 @@ VoiceSelect.prototype = {
}
},
- _select: function(option, suppressEvent = false) {
+ _select(option, suppressEvent = false) {
let oldSelected = this.selected;
if (oldSelected) {
oldSelected.removeAttribute("aria-selected");
@@ -233,7 +234,7 @@ VoiceSelect.prototype = {
}
},
- _updateDropdownHeight: function(now) {
+ _updateDropdownHeight(now) {
let updateInner = () => {
let winHeight = this._win.innerHeight;
let listbox = this.listbox;
@@ -252,7 +253,7 @@ VoiceSelect.prototype = {
}
},
- _getOptionFromValue: function(value) {
+ _getOptionFromValue(value) {
return Array.from(this.options).find(o => o.dataset.value === value);
},
diff --git a/toolkit/components/osfile/modules/osfile_async_front.jsm b/toolkit/components/osfile/modules/osfile_async_front.jsm
index 181471cd8..964e53084 100644
--- a/toolkit/components/osfile/modules/osfile_async_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_async_front.jsm
@@ -753,13 +753,13 @@ File.prototype = {
};
-if (SharedAll.Constants.Sys.Name != "Android" && SharedAll.Constants.Sys.Name != "Gonk") {
+if (SharedAll.Constants.Sys.Name != "Android") {
/**
* Set the last access and modification date of the file.
* The time stamp resolution is 1 second at best, but might be worse
* depending on the platform.
*
- * WARNING: This method is not implemented on Android/B2G. On Android/B2G,
+ * WARNING: This method is not implemented on Android. On Android,
* you should use File.setDates instead.
*
* @return {promise}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_constants.js b/toolkit/components/osfile/tests/xpcshell/test_constants.js
index e92f33ab8..5b91484bd 100644
--- a/toolkit/components/osfile/tests/xpcshell/test_constants.js
+++ b/toolkit/components/osfile/tests/xpcshell/test_constants.js
@@ -15,12 +15,7 @@ add_task(function* check_definition() {
do_check_true(OS.Constants.Path!=null);
do_check_true(OS.Constants.Sys!=null);
//check system name
- if (OS.Constants.Sys.Name == "Gonk") {
- // Services.appinfo.OS doesn't know the difference between Gonk and Android
- do_check_eq(Services.appinfo.OS, "Android");
- } else {
- do_check_eq(Services.appinfo.OS, OS.Constants.Sys.Name);
- }
+ do_check_eq(Services.appinfo.OS, OS.Constants.Sys.Name);
//check if using DEBUG build
if (Components.classes["@mozilla.org/xpcom/debug;1"].getService(Components.interfaces.nsIDebug2).isDebugBuild == true) {
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js
index 17d3afa7c..6a5ecd5e3 100644
--- a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js
@@ -96,8 +96,8 @@ add_task(function* test_nonproto() {
// Prototypical tests, operating on |File| handles.
add_task(function* test_proto() {
- if (OS.Constants.Sys.Name == "Android" || OS.Constants.Sys.Name == "Gonk") {
- do_print("File.prototype.setDates is not implemented for Android/B2G");
+ if (OS.Constants.Sys.Name == "Android") {
+ do_print("File.prototype.setDates is not implemented for Android");
do_check_eq(OS.File.prototype.setDates, undefined);
return;
}
@@ -107,7 +107,7 @@ add_task(function* test_proto() {
"test_osfile_async_setDates_proto.tmp");
yield OS.File.writeAtomic(path, new Uint8Array(1));
- try {
+ try {
let fd = yield OS.File.open(path, {write: true});
try {
diff --git a/toolkit/components/osfile/tests/xpcshell/test_path_constants.js b/toolkit/components/osfile/tests/xpcshell/test_path_constants.js
index c0057c750..9b9868bb2 100644
--- a/toolkit/components/osfile/tests/xpcshell/test_path_constants.js
+++ b/toolkit/components/osfile/tests/xpcshell/test_path_constants.js
@@ -58,7 +58,7 @@ add_task(function* test_simple_paths() {
// Test presence of paths that only exist on Desktop platforms
add_task(function* test_desktop_paths() {
- if (OS.Constants.Sys.Name == "Android" || OS.Constants.Sys.Name == "Gonk") {
+ if (OS.Constants.Sys.Name == "Android") {
return;
}
do_check_true(!!OS.Constants.Path.desktopDir);
diff --git a/toolkit/components/printing/content/simplifyMode.css b/toolkit/components/printing/content/simplifyMode.css
index 2a8706c75..d02f216dc 100644
--- a/toolkit/components/printing/content/simplifyMode.css
+++ b/toolkit/components/printing/content/simplifyMode.css
@@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* This file defines specific rules for print preview when using simplify mode.
- * These rules already exist on aboutReaderControls.css, however, we decoupled it
+ * These rules already exist on aboutReader.css, however, we decoupled it
* from the original file so we don't need to load a bunch of extra queries that
* will not take effect when using the simplify page checkbox. This file defines
* styling for title and author on the header element. */
diff --git a/toolkit/components/reader/AboutReader.jsm b/toolkit/components/reader/AboutReader.jsm
index 1fb9db123..fb82e5789 100644
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -15,12 +15,12 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AsyncPrefs", "resource://gre/modules/AsyncPrefs.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NarrateControls", "resource://gre/modules/narrate/NarrateControls.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
-var AboutReader = function(mm, win, articlePromise) {
+var AboutReader = function(win, articlePromise) {
let url = this._getOriginalUrl(win);
if (!(url.startsWith("http://") || url.startsWith("https://"))) {
let errorMsg = "Only http:// and https:// URLs can be loaded in about:reader.";
@@ -33,57 +33,59 @@ var AboutReader = function(mm, win, articlePromise) {
let doc = win.document;
- this._mm = mm;
- this._mm.addMessageListener("Reader:CloseDropdown", this);
- this._mm.addMessageListener("Reader:AddButton", this);
- this._mm.addMessageListener("Reader:RemoveButton", this);
- this._mm.addMessageListener("Reader:GetStoredArticleData", this);
-
this._docRef = Cu.getWeakReference(doc);
this._winRef = Cu.getWeakReference(win);
this._innerWindowId = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
this._article = null;
+ this._languagePromise = new Promise(resolve => {
+ this._foundLanguage = resolve;
+ });
if (articlePromise) {
this._articlePromise = articlePromise;
}
- this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header"));
- this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain"));
- this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title"));
- this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits"));
- this._contentElementRef = Cu.getWeakReference(doc.getElementById("moz-reader-content"));
- this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar"));
- this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message"));
+ this._headerElementRef = Cu.getWeakReference(doc.querySelector(".reader-header"));
+ this._domainElementRef = Cu.getWeakReference(doc.querySelector(".reader-domain"));
+ this._titleElementRef = Cu.getWeakReference(doc.querySelector(".reader-title"));
+ this._readTimeElementRef = Cu.getWeakReference(doc.querySelector(".reader-estimated-time"));
+ this._creditsElementRef = Cu.getWeakReference(doc.querySelector(".reader-credits"));
+ this._contentElementRef = Cu.getWeakReference(doc.querySelector(".moz-reader-content"));
+ this._toolbarElementRef = Cu.getWeakReference(doc.querySelector(".reader-toolbar"));
+ this._messageElementRef = Cu.getWeakReference(doc.querySelector(".reader-message"));
+ this._containerElementRef = Cu.getWeakReference(doc.querySelector(".container"));
this._scrollOffset = win.pageYOffset;
- doc.addEventListener("click", this, false);
+ doc.addEventListener("click", this);
+
+ win.addEventListener("pagehide", this);
+ win.addEventListener("scroll", this);
+ win.addEventListener("resize", this);
- win.addEventListener("pagehide", this, false);
- win.addEventListener("scroll", this, false);
- win.addEventListener("resize", this, false);
+ win.addEventListener("AboutReaderAddButton", this, false, true);
+ win.addEventListener("AboutReaderRemoveButton", this, false, true);
Services.obs.addObserver(this, "inner-window-destroyed", false);
- doc.addEventListener("visibilitychange", this, false);
+ doc.addEventListener("visibilitychange", this);
this._setupStyleDropdown();
this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close");
- const gIsFirefoxDesktop = Services.appinfo.ID == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
- if (gIsFirefoxDesktop) {
- // we're ready for any external setup, send a signal for that.
- this._mm.sendAsyncMessage("Reader:OnSetup");
- }
+ // we're ready for any external setup, send a signal for that.
+ doc.dispatchEvent(
+ new win.CustomEvent("AboutReaderOnSetup", { bubbles: true, cancelable: false }));
let colorSchemeValues = JSON.parse(Services.prefs.getCharPref("reader.color_scheme.values"));
let colorSchemeOptions = colorSchemeValues.map((value) => {
- return { name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
- value: value,
- itemClass: value + "-button" };
+ return {
+ name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
+ value,
+ itemClass: value + "-button"
+ };
});
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
@@ -114,7 +116,7 @@ var AboutReader = function(mm, win, articlePromise) {
this._setupLineHeightButtons();
if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
- new NarrateControls(mm, win);
+ new NarrateControls(win, this._languagePromise);
}
this._loadArticle();
@@ -146,6 +148,10 @@ AboutReader.prototype = {
return this._titleElementRef.get();
},
+ get _readTimeElement() {
+ return this._readTimeElementRef.get();
+ },
+
get _creditsElement() {
return this._creditsElementRef.get();
},
@@ -162,6 +168,10 @@ AboutReader.prototype = {
return this._messageElementRef.get();
},
+ get _containerElement() {
+ return this._containerElementRef.get();
+ },
+
get _isToolbarVertical() {
if (this._toolbarVertical !== undefined) {
return this._toolbarVertical;
@@ -178,72 +188,31 @@ AboutReader.prototype = {
return _viewId;
},
- receiveMessage: function (message) {
- switch (message.name) {
- // Triggered by Android user pressing BACK while the banner font-dropdown is open.
- case "Reader:CloseDropdown": {
- // Just close it.
- this._closeDropdowns();
- break;
- }
-
- case "Reader:AddButton": {
- if (message.data.id && message.data.image &&
- !this._doc.getElementById(message.data.id)) {
- let btn = this._doc.createElement("button");
- btn.setAttribute("class", "button");
- btn.setAttribute("style", "background-image: url('" + message.data.image + "')");
- btn.setAttribute("id", message.data.id);
- if (message.data.title)
- btn.setAttribute("title", message.data.title);
- if (message.data.text)
- btn.textContent = message.data.text;
- let tb = this._doc.getElementById("reader-toolbar");
- tb.appendChild(btn);
- this._setupButton(message.data.id, button => {
- this._mm.sendAsyncMessage("Reader:Clicked-" + button.getAttribute("id"), { article: this._article });
- });
- }
- break;
- }
- case "Reader:RemoveButton": {
- if (message.data.id) {
- let btn = this._doc.getElementById(message.data.id);
- if (btn)
- btn.remove();
- }
- break;
- }
- case "Reader:GetStoredArticleData": {
- this._mm.sendAsyncMessage("Reader:StoredArticleData", { article: this._article });
- }
- }
- },
-
- handleEvent: function(aEvent) {
+ handleEvent(aEvent) {
if (!aEvent.isTrusted)
return;
switch (aEvent.type) {
case "click":
let target = aEvent.target;
- if (target.classList.contains('dropdown-toggle')) {
+ if (target.classList.contains("dropdown-toggle")) {
this._toggleDropdownClicked(aEvent);
- } else if (!target.closest('.dropdown-popup')) {
+ } else if (!target.closest(".dropdown-popup")) {
this._closeDropdowns();
}
+ if (target.tagName == "A" && !target.classList.contains("reader-domain")) {
+ this._linkClicked(aEvent);
+ }
break;
case "scroll":
this._closeDropdowns(true);
- let isScrollingUp = this._scrollOffset > aEvent.pageY;
- this._setSystemUIVisibility(isScrollingUp);
this._scrollOffset = aEvent.pageY;
break;
case "resize":
this._updateImageMargins();
if (this._isToolbarVertical) {
this._win.setTimeout(() => {
- for (let dropdown of this._doc.querySelectorAll('.dropdown.open')) {
+ for (let dropdown of this._doc.querySelectorAll(".dropdown.open")) {
this._updatePopupPosition(dropdown);
}
}, 0);
@@ -261,35 +230,57 @@ AboutReader.prototype = {
case "pagehide":
// Close the Banners Font-dropdown, cleanup Android BackPressListener.
this._closeDropdowns();
-
- this._mm.removeMessageListener("Reader:CloseDropdown", this);
- this._mm.removeMessageListener("Reader:AddButton", this);
- this._mm.removeMessageListener("Reader:RemoveButton", this);
- this._mm.removeMessageListener("Reader:GetStoredArticleData", this);
this._windowUnloaded = true;
break;
+
+ case "AboutReaderAddButton": {
+ if (aEvent.detail.id && aEvent.detail.image &&
+ !this._doc.getElementById(aEvent.detail.id)) {
+ let btn = this._doc.createElement("button");
+ btn.setAttribute("class", "button " + aEvent.detail.id);
+ btn.setAttribute("style", "background-image: url('" + aEvent.detail.image + "')");
+ btn.setAttribute("id", aEvent.detail.id);
+ if (aEvent.detail.title)
+ btn.setAttribute("title", aEvent.detail.title);
+ if (aEvent.detail.text)
+ btn.textContent = aEvent.detail.text;
+ let tb = this._toolbarElement;
+ tb.appendChild(btn);
+ this._setupButton(aEvent.detail.id, button => {
+ var data = { article: this._article };
+ this._doc.dispatchEvent(
+ new this._win.CustomEvent("AboutReaderButtonClicked-" + button.getAttribute("id"), {detail: data, bubbles: true, cancelable: false}));
+ });
+ }
+ break;
+ }
+
+ case "AboutReaderRemoveButton": {
+ if (aEvent.detail.id) {
+ let btn = this._doc.getElementById(aEvent.detail.id);
+ if (btn)
+ btn.remove();
+ }
+ break;
+ }
}
},
- observe: function(subject, topic, data) {
+ observe(subject, topic, data) {
if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) {
return;
}
- Services.obs.removeObserver(this, "inner-window-destroyed", false);
-
- this._mm.removeMessageListener("Reader:CloseDropdown", this);
- this._mm.removeMessageListener("Reader:AddButton", this);
- this._mm.removeMessageListener("Reader:RemoveButton", this);
+ Services.obs.removeObserver(this, "inner-window-destroyed");
this._windowUnloaded = true;
},
- _onReaderClose: function() {
- ReaderMode.leaveReaderMode(this._mm.docShell, this._win);
+ _onReaderClose() {
+ ReaderMode.leaveReaderMode(this._win.document.docShell, this._win);
},
- _setFontSize: function(newFontSize) {
- let containerClasses = this._doc.getElementById("container").classList;
+ _setFontSize(newFontSize) {
+ let containerClasses = this._containerElement.classList;
if (this._fontSize > 0)
containerClasses.remove("font-size" + this._fontSize);
@@ -299,19 +290,19 @@ AboutReader.prototype = {
return AsyncPrefs.set("reader.font_size", this._fontSize);
},
- _setupFontSizeButtons: function() {
+ _setupFontSizeButtons() {
const FONT_SIZE_MIN = 1;
const FONT_SIZE_MAX = 9;
// Sample text shown in Android UI.
- let sampleText = this._doc.getElementById("font-size-sample");
+ let sampleText = this._doc.querySelector(".font-size-sample");
sampleText.textContent = gStrings.GetStringFromName("aboutReader.fontTypeSample");
let currentSize = Services.prefs.getIntPref("reader.font_size");
currentSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize));
- let plusButton = this._doc.getElementById("font-size-plus");
- let minusButton = this._doc.getElementById("font-size-minus");
+ let plusButton = this._doc.querySelector(".plus-button");
+ let minusButton = this._doc.querySelector(".minus-button");
function updateControls() {
if (currentSize === FONT_SIZE_MIN) {
@@ -360,8 +351,8 @@ AboutReader.prototype = {
}, true);
},
- _setContentWidth: function(newContentWidth) {
- let containerClasses = this._doc.getElementById("container").classList;
+ _setContentWidth(newContentWidth) {
+ let containerClasses = this._containerElement.classList;
if (this._contentWidth > 0)
containerClasses.remove("content-width" + this._contentWidth);
@@ -371,15 +362,15 @@ AboutReader.prototype = {
return AsyncPrefs.set("reader.content_width", this._contentWidth);
},
- _setupContentWidthButtons: function() {
+ _setupContentWidthButtons() {
const CONTENT_WIDTH_MIN = 1;
const CONTENT_WIDTH_MAX = 9;
let currentContentWidth = Services.prefs.getIntPref("reader.content_width");
currentContentWidth = Math.max(CONTENT_WIDTH_MIN, Math.min(CONTENT_WIDTH_MAX, currentContentWidth));
- let plusButton = this._doc.getElementById("content-width-plus");
- let minusButton = this._doc.getElementById("content-width-minus");
+ let plusButton = this._doc.querySelector(".content-width-plus-button");
+ let minusButton = this._doc.querySelector(".content-width-minus-button");
function updateControls() {
if (currentContentWidth === CONTENT_WIDTH_MIN) {
@@ -428,8 +419,8 @@ AboutReader.prototype = {
}, true);
},
- _setLineHeight: function(newLineHeight) {
- let contentClasses = this._doc.getElementById("moz-reader-content").classList;
+ _setLineHeight(newLineHeight) {
+ let contentClasses = this._contentElement.classList;
if (this._lineHeight > 0)
contentClasses.remove("line-height" + this._lineHeight);
@@ -439,15 +430,15 @@ AboutReader.prototype = {
return AsyncPrefs.set("reader.line_height", this._lineHeight);
},
- _setupLineHeightButtons: function() {
+ _setupLineHeightButtons() {
const LINE_HEIGHT_MIN = 1;
const LINE_HEIGHT_MAX = 9;
let currentLineHeight = Services.prefs.getIntPref("reader.line_height");
currentLineHeight = Math.max(LINE_HEIGHT_MIN, Math.min(LINE_HEIGHT_MAX, currentLineHeight));
- let plusButton = this._doc.getElementById("line-height-plus");
- let minusButton = this._doc.getElementById("line-height-minus");
+ let plusButton = this._doc.querySelector(".line-height-plus-button");
+ let minusButton = this._doc.querySelector(".line-height-minus-button");
function updateControls() {
if (currentLineHeight === LINE_HEIGHT_MIN) {
@@ -496,7 +487,7 @@ AboutReader.prototype = {
}, true);
},
- _handleDeviceLight: function(newLux) {
+ _handleDeviceLight(newLux) {
// Desired size of the this._luxValues array.
let luxValuesSize = 10;
// Add new lux value at the front of the array.
@@ -513,7 +504,7 @@ AboutReader.prototype = {
return;
}
// Holds the average of the lux values collected in this._luxValues.
- let averageLuxValue = this._totalLux/luxValuesSize;
+ let averageLuxValue = this._totalLux / luxValuesSize;
this._updateColorScheme(averageLuxValue);
// Pop the oldest value off the array.
@@ -522,7 +513,7 @@ AboutReader.prototype = {
this._totalLux -= oldLux;
},
- _handleVisibilityChange: function() {
+ _handleVisibilityChange() {
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
if (colorScheme != "auto") {
return;
@@ -533,19 +524,19 @@ AboutReader.prototype = {
},
// Setup or teardown the ambient light tracking system.
- _enableAmbientLighting: function(enable) {
+ _enableAmbientLighting(enable) {
if (enable) {
- this._win.addEventListener("devicelight", this, false);
+ this._win.addEventListener("devicelight", this);
this._luxValues = [];
this._totalLux = 0;
} else {
- this._win.removeEventListener("devicelight", this, false);
+ this._win.removeEventListener("devicelight", this);
delete this._luxValues;
delete this._totalLux;
}
},
- _updateColorScheme: function(luxValue) {
+ _updateColorScheme(luxValue) {
// Upper bound value for "dark" color scheme beyond which it changes to "light".
let upperBoundDark = 50;
// Lower bound value for "light" color scheme beyond which it changes to "dark".
@@ -564,7 +555,7 @@ AboutReader.prototype = {
this._setColorScheme("light");
},
- _setColorScheme: function(newColorScheme) {
+ _setColorScheme(newColorScheme) {
// "auto" is not a real color scheme
if (this._colorScheme === newColorScheme || newColorScheme === "auto")
return;
@@ -580,14 +571,14 @@ AboutReader.prototype = {
// Pref values include "dark", "light", and "auto", which automatically switches
// between light and dark color schemes based on the ambient light level.
- _setColorSchemePref: function(colorSchemePref) {
+ _setColorSchemePref(colorSchemePref) {
this._enableAmbientLighting(colorSchemePref === "auto");
this._setColorScheme(colorSchemePref);
AsyncPrefs.set("reader.color_scheme", colorSchemePref);
},
- _setFontType: function(newFontType) {
+ _setFontType(newFontType) {
if (this._fontType === newFontType)
return;
@@ -602,20 +593,34 @@ AboutReader.prototype = {
AsyncPrefs.set("reader.font_type", this._fontType);
},
- _setSystemUIVisibility: function(visible) {
- this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible: visible });
+ _setToolbarVisibility(visible) {
+ let tb = this._toolbarElement;
+
+ if (visible) {
+ if (tb.style.opacity != "1") {
+ tb.removeAttribute("hidden");
+ tb.style.opacity = "1";
+ }
+ } else if (tb.style.opacity != "0") {
+ tb.addEventListener("transitionend", evt => {
+ if (tb.style.opacity == "0") {
+ tb.setAttribute("hidden", "");
+ }
+ }, { once: true });
+ tb.style.opacity = "0";
+ }
},
- _loadArticle: Task.async(function* () {
+ async _loadArticle() {
let url = this._getOriginalUrl();
this._showProgressDelayed();
let article;
if (this._articlePromise) {
- article = yield this._articlePromise;
+ article = await this._articlePromise;
} else {
try {
- article = yield this._getArticle(url);
+ article = await this._getArticle(url);
} catch (e) {
if (e && e.newURL) {
let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
@@ -638,47 +643,37 @@ AboutReader.prototype = {
}
this._showContent(article);
- }),
-
- _getArticle: function(url) {
- return new Promise((resolve, reject) => {
- let listener = (message) => {
- this._mm.removeMessageListener("Reader:ArticleData", listener);
- if (message.data.newURL) {
- reject({ newURL: message.data.newURL });
- return;
- }
- resolve(message.data.article);
- };
- this._mm.addMessageListener("Reader:ArticleData", listener);
- this._mm.sendAsyncMessage("Reader:ArticleGet", { url: url });
- });
},
- _requestFavicon: function() {
- let handleFaviconReturn = (message) => {
- this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn);
- this._loadFavicon(message.data.url, message.data.faviconUrl);
- };
+ _getArticle(url) {
+ return ReaderMode.downloadAndParseDocument(url);
+ },
- this._mm.addMessageListener("Reader:FaviconReturn", handleFaviconReturn);
- this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url });
+ _requestFavicon() {
+ let faviconUrl = PlacesUtils.promiseFaviconLinkUrl(this._article.url);
+ var self = this;
+ faviconUrl.then(function onResolution(favicon) {
+ self._loadFavicon(self._article.url, favicon.path.replace(/^favicon:/, ""));
+ },
+ function onRejection(reason) {
+ Cu.reportError("Error requesting favicon URL for about:reader content: " + reason);
+ }).catch(Cu.reportError);
},
- _loadFavicon: function(url, faviconUrl) {
+ _loadFavicon(url, faviconUrl) {
if (this._article.url !== url)
return;
let doc = this._doc;
- let link = doc.createElement('link');
- link.rel = 'shortcut icon';
+ let link = doc.createElement("link");
+ link.rel = "shortcut icon";
link.href = faviconUrl;
- doc.getElementsByTagName('head')[0].appendChild(link);
+ doc.getElementsByTagName("head")[0].appendChild(link);
},
- _updateImageMargins: function() {
+ _updateImageMargins() {
let windowWidth = this._win.innerWidth;
let bodyWidth = this._doc.body.clientWidth;
@@ -691,7 +686,7 @@ AboutReader.prototype = {
}
// If the image is at least half as wide as the body, center it on desktop.
- if (img.naturalWidth >= bodyWidth/2) {
+ if (img.naturalWidth >= bodyWidth / 2) {
img.setAttribute("moz-reader-center", true);
} else {
img.removeAttribute("moz-reader-center");
@@ -713,30 +708,32 @@ AboutReader.prototype = {
},
_maybeSetTextDirection: function Read_maybeSetTextDirection(article) {
- if (!article.dir)
- return;
+ if (article.dir) {
+ // Set "dir" attribute on content
+ this._contentElement.setAttribute("dir", article.dir);
+ this._headerElement.setAttribute("dir", article.dir);
+
+ // The native locale could be set differently than the article's text direction.
+ var localeDirection = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
+ this._readTimeElement.setAttribute("dir", localeDirection);
+ this._readTimeElement.style.textAlign = article.dir == "rtl" ? "right" : "left";
+ }
+ },
- // Set "dir" attribute on content
- this._contentElement.setAttribute("dir", article.dir);
- this._headerElement.setAttribute("dir", article.dir);
- },
-
- _fixLocalLinks() {
- // We need to do this because preprocessing the content through nsIParserUtils
- // gives back a DOM with a <base> element. That influences how these URLs get
- // resolved, making them no longer match the document URI (which is
- // about:reader?url=...). To fix this, make all the hash URIs absolute. This
- // is hacky, but the alternative of removing the base element has potential
- // security implications if Readability has not successfully made all the URLs
- // absolute, so we pick just fixing these in-document links explicitly.
- let localLinks = this._contentElement.querySelectorAll("a[href^='#']");
- for (let localLink of localLinks) {
- // Have to get the attribute because .href provides an absolute URI.
- localLink.href = this._doc.documentURI + localLink.getAttribute("href");
+ _formatReadTime(slowEstimate, fastEstimate) {
+ let displayStringKey = "aboutReader.estimatedReadTimeRange1";
+
+ // only show one reading estimate when they are the same value
+ if (slowEstimate == fastEstimate) {
+ displayStringKey = "aboutReader.estimatedReadTimeValue1";
}
+
+ return PluralForm.get(slowEstimate, gStrings.GetStringFromName(displayStringKey))
+ .replace("#1", fastEstimate)
+ .replace("#2", slowEstimate);
},
- _showError: function() {
+ _showError() {
this._headerElement.style.display = "none";
this._contentElement.style.display = "none";
@@ -746,11 +743,16 @@ AboutReader.prototype = {
this._doc.title = errorMessage;
+ this._doc.documentElement.dataset.isError = true;
+
this._error = true;
+
+ this._doc.dispatchEvent(
+ new this._win.CustomEvent("AboutReaderContentError", { bubbles: true, cancelable: false }));
},
// This function is the JS version of Java's StringUtils.stripCommonSubdomains.
- _stripHost: function(host) {
+ _stripHost(host) {
if (!host)
return host;
@@ -766,17 +768,18 @@ AboutReader.prototype = {
return host.substring(start);
},
- _showContent: function(article) {
+ _showContent(article) {
this._messageElement.style.display = "none";
this._article = article;
this._domainElement.href = article.url;
- let articleUri = Services.io.newURI(article.url, null, null);
+ let articleUri = Services.io.newURI(article.url);
this._domainElement.textContent = this._stripHost(articleUri.host);
this._creditsElement.textContent = article.byline;
this._titleElement.textContent = article.title;
+ this._readTimeElement.textContent = this._formatReadTime(article.readingTimeMinsSlow, article.readingTimeMinsFast);
this._doc.title = article.title;
this._headerElement.style.display = "block";
@@ -787,8 +790,8 @@ AboutReader.prototype = {
false, articleUri, this._contentElement);
this._contentElement.innerHTML = "";
this._contentElement.appendChild(contentFragment);
- this._fixLocalLinks();
this._maybeSetTextDirection(article);
+ this._foundLanguage(article.language);
this._contentElement.style.display = "block";
this._updateImageMargins();
@@ -804,13 +807,13 @@ AboutReader.prototype = {
new this._win.CustomEvent("AboutReaderContentReady", { bubbles: true, cancelable: false }));
},
- _hideContent: function() {
+ _hideContent() {
this._headerElement.style.display = "none";
this._contentElement.style.display = "none";
},
- _showProgressDelayed: function() {
- this._win.setTimeout(function() {
+ _showProgressDelayed() {
+ this._win.setTimeout(() => {
// No need to show progress if the article has been loaded,
// if the window has been unloaded, or if there was an error
// trying to load the article.
@@ -823,20 +826,20 @@ AboutReader.prototype = {
this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading2");
this._messageElement.style.display = "block";
- }.bind(this), 300);
+ }, 300);
},
/**
* Returns the original article URL for this about:reader view.
*/
- _getOriginalUrl: function(win) {
+ _getOriginalUrl(win) {
let url = win ? win.location.href : this._win.location.href;
return ReaderMode.getOriginalUrl(url) || url;
},
- _setupSegmentedButton: function(id, options, initialValue, callback) {
+ _setupSegmentedButton(id, options, initialValue, callback) {
let doc = this._doc;
- let segmentedButton = doc.getElementById(id);
+ let segmentedButton = doc.getElementsByClassName(id)[0];
for (let i = 0; i < options.length; i++) {
let option = options[i];
@@ -867,10 +870,6 @@ AboutReader.prototype = {
aEvent.stopPropagation();
- // Just pass the ID of the button as an extra and hope the ID doesn't change
- // unless the context changes
- UITelemetry.addEvent("action.1", "button", null, id);
-
let items = segmentedButton.children;
for (let j = items.length - 1; j >= 0; j--) {
items[j].classList.remove("selected");
@@ -878,19 +877,19 @@ AboutReader.prototype = {
item.classList.add("selected");
callback(option.value);
- }.bind(this), true);
+ }, true);
if (option.value === initialValue)
item.classList.add("selected");
}
},
- _setupButton: function(id, callback, titleEntity, textEntity) {
+ _setupButton(id, callback, titleEntity, textEntity) {
if (titleEntity) {
this._setButtonTip(id, titleEntity);
}
- let button = this._doc.getElementById(id);
+ let button = this._doc.getElementsByClassName(id)[0];
if (textEntity) {
button.textContent = gStrings.GetStringFromName(textEntity);
}
@@ -910,17 +909,17 @@ AboutReader.prototype = {
* and dynamically as button state changes.
* @param Localizable string providing UI element usage tip.
*/
- _setButtonTip: function(id, titleEntity) {
- let button = this._doc.getElementById(id);
+ _setButtonTip(id, titleEntity) {
+ let button = this._doc.getElementsByClassName(id)[0];
button.setAttribute("title", gStrings.GetStringFromName(titleEntity));
},
- _setupStyleDropdown: function() {
- let dropdownToggle = this._doc.querySelector("#style-dropdown .dropdown-toggle");
+ _setupStyleDropdown() {
+ let dropdownToggle = this._doc.querySelector(".style-dropdown .dropdown-toggle");
dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
},
- _updatePopupPosition: function(dropdown) {
+ _updatePopupPosition(dropdown) {
let dropdownToggle = dropdown.querySelector(".dropdown-toggle");
let dropdownPopup = dropdown.querySelector(".dropdown-popup");
@@ -931,8 +930,8 @@ AboutReader.prototype = {
dropdownPopup.style.top = popupTop + "px";
},
- _toggleDropdownClicked: function(event) {
- let dropdown = event.target.closest('.dropdown');
+ _toggleDropdownClicked(event) {
+ let dropdown = event.target.closest(".dropdown");
if (!dropdown)
return;
@@ -952,16 +951,13 @@ AboutReader.prototype = {
/*
* If the ReaderView banner font-dropdown is closed, open it.
*/
- _openDropdown: function(dropdown) {
+ _openDropdown(dropdown) {
if (dropdown.classList.contains("open")) {
return;
}
this._closeDropdowns();
-
- // Trigger BackPressListener initialization in Android.
dropdown.classList.add("open");
- this._mm.sendAsyncMessage("Reader:DropdownOpened", this.viewId);
},
/*
@@ -969,7 +965,7 @@ AboutReader.prototype = {
* dropdowns because the page is scrolling, allow popups to stay open with
* the keep-open class.
*/
- _closeDropdowns: function(scrolling) {
+ _closeDropdowns(scrolling) {
let selector = ".dropdown.open";
if (scrolling) {
selector += ":not(.keep-open)";
@@ -979,10 +975,17 @@ AboutReader.prototype = {
for (let dropdown of openDropdowns) {
dropdown.classList.remove("open");
}
+ },
- // Trigger BackPressListener cleanup in Android.
- if (openDropdowns.length) {
- this._mm.sendAsyncMessage("Reader:DropdownClosed", this.viewId);
+ /*
+ * Override link handling for same-page references so we don't exit Reader View.
+ */
+ _linkClicked(event) {
+ var originalUrl = Services.io.newURI(this._getOriginalUrl(), null, null);
+ var targetUrl = Services.io.newURI(event.target.href, null, null);
+ if (originalUrl.specIgnoringRef == targetUrl.specIgnoringRef) {
+ event.preventDefault();
+ this._goToReference(targetUrl.ref);
}
},
diff --git a/toolkit/components/reader/JSDOMParser.js b/toolkit/components/reader/JSDOMParser.js
index 853649775..38f59c4ea 100644
--- a/toolkit/components/reader/JSDOMParser.js
+++ b/toolkit/components/reader/JSDOMParser.js
@@ -1017,46 +1017,6 @@
}
},
- readScript: function (node) {
- while (this.currentChar < this.html.length) {
- var c = this.nextChar();
- var nextC = this.peekNext();
- if (c === "<") {
- if (nextC === "!" || nextC === "?") {
- // We're still before the ! or ? that is starting this comment:
- this.currentChar++;
- node.appendChild(this.discardNextComment());
- continue;
- }
- if (nextC === "/" && this.html.substr(this.currentChar, 8 /*"/script>".length */).toLowerCase() == "/script>") {
- // Go back before the '<' so we find the end tag.
- this.currentChar--;
- // Done with this script tag, the caller will close:
- return;
- }
- }
- // Either c wasn't a '<' or it was but we couldn't find either a comment
- // or a closing script tag, so we should just parse as text until the next one
- // comes along:
-
- var haveTextNode = node.lastChild && node.lastChild.nodeType === Node.TEXT_NODE;
- var textNode = haveTextNode ? node.lastChild : new Text();
- var n = this.html.indexOf("<", this.currentChar);
- // Decrement this to include the current character *afterwards* so we don't get stuck
- // looking for the same < all the time.
- this.currentChar--;
- if (n === -1) {
- textNode.innerHTML += this.html.substring(this.currentChar, this.html.length);
- this.currentChar = this.html.length;
- } else {
- textNode.innerHTML += this.html.substring(this.currentChar, n);
- this.currentChar = n;
- }
- if (!haveTextNode)
- node.appendChild(textNode);
- }
- },
-
discardNextComment: function() {
if (this.match("--")) {
this.discardTo("-->");
@@ -1131,11 +1091,7 @@
// If this isn't a void Element, read its child nodes
if (!closed) {
- if (localName == "script") {
- this.readScript(node);
- } else {
- this.readChildren(node);
- }
+ this.readChildren(node);
var closingTag = "</" + localName + ">";
if (!this.match(closingTag)) {
this.error("expected '" + closingTag + "' and got " + this.html.substr(this.currentChar, closingTag.length));
diff --git a/toolkit/components/reader/Readability.js b/toolkit/components/reader/Readability.js
index 491461a8e..04949dc61 100644
--- a/toolkit/components/reader/Readability.js
+++ b/toolkit/components/reader/Readability.js
@@ -38,32 +38,22 @@ function Readability(uri, doc, options) {
this._uri = uri;
this._doc = doc;
- this._biggestFrame = false;
+ this._articleTitle = null;
this._articleByline = null;
this._articleDir = null;
- // Configureable options
+ // Configurable options
this._debug = !!options.debug;
this._maxElemsToParse = options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE;
this._nbTopCandidates = options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES;
- this._maxPages = options.maxPages || this.DEFAULT_MAX_PAGES;
+ this._wordThreshold = options.wordThreshold || this.DEFAULT_WORD_THRESHOLD;
+ this._classesToPreserve = this.CLASSES_TO_PRESERVE.concat(options.classesToPreserve || []);
// Start with all flags set
this._flags = this.FLAG_STRIP_UNLIKELYS |
this.FLAG_WEIGHT_CLASSES |
this.FLAG_CLEAN_CONDITIONALLY;
- // The list of pages we've parsed in this call of readability,
- // for autopaging. As a key store for easier searching.
- this._parsedPages = {};
-
- // A list of the ETag headers of pages we've parsed, in case they happen to match,
- // we'll know it's a duplicate.
- this._pageETags = {};
-
- // Make an AJAX request for each page and append it to the document.
- this._curPageNum = 1;
-
var logEl;
// Control whether log messages are sent to the console
@@ -82,12 +72,12 @@ function Readability(uri, doc, options) {
return rv + elDesc;
};
this.log = function () {
- if (typeof dump !== undefined) {
+ if (typeof dump !== "undefined") {
var msg = Array.prototype.map.call(arguments, function(x) {
return (x && x.nodeName) ? logEl(x) : x;
}).join(" ");
dump("Reader: (Readability) " + msg + "\n");
- } else if (typeof console !== undefined) {
+ } else if (typeof console !== "undefined") {
var args = ["Reader: (Readability) "].concat(arguments);
console.log.apply(console, args);
}
@@ -109,20 +99,19 @@ Readability.prototype = {
// tight the competition is among candidates.
DEFAULT_N_TOP_CANDIDATES: 5,
- // The maximum number of pages to loop through before we call
- // it quits and just show a link.
- DEFAULT_MAX_PAGES: 5,
-
// Element tags to score by default.
DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),
+ // The default number of words an article must have in order to return a result
+ DEFAULT_WORD_THRESHOLD: 500,
+
// All of the regular expressions in use within readability.
// Defined up here so we don't instantiate them repeatedly in loops.
REGEXPS: {
- unlikelyCandidates: /banner|combx|comment|community|disqus|extra|foot|header|menu|modal|related|remark|rss|share|shoutbox|sidebar|skyscraper|sponsor|ad-break|agegate|pagination|pager|popup/i,
+ unlikelyCandidates: /banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,
- negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|modal|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
+ negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
byline: /byline|author|dateline|writtenby|p-author/i,
replaceFonts: /<(\/?)font[^>]*>/gi,
@@ -138,6 +127,13 @@ Readability.prototype = {
ALTER_TO_DIV_EXCEPTIONS: ["DIV", "ARTICLE", "SECTION", "P"],
+ PRESENTATIONAL_ATTRIBUTES: [ "align", "background", "bgcolor", "border", "cellpadding", "cellspacing", "frame", "hspace", "rules", "style", "valign", "vspace" ],
+
+ DEPRECATED_SIZE_ATTRIBUTE_ELEMS: [ "TABLE", "TH", "TD", "HR", "PRE" ],
+
+ // These are the classes that readability sets itself.
+ CLASSES_TO_PRESERVE: [ "readability-styled", "page" ],
+
/**
* Run any post-process modifications to article content as necessary.
*
@@ -147,6 +143,9 @@ Readability.prototype = {
_postProcessContent: function(articleContent) {
// Readability cannot open relative uris so we convert them to absolute uris.
this._fixRelativeUris(articleContent);
+
+ // Remove classes.
+ this._cleanClasses(articleContent);
},
/**
@@ -155,8 +154,8 @@ Readability.prototype = {
*
* If function is not passed, removes all the nodes in node list.
*
- * @param NodeList nodeList The no
- * @param Function filterFn
+ * @param NodeList nodeList The nodes to operate on
+ * @param Function filterFn the function to use as a filter
* @return void
*/
_removeNodes: function(nodeList, filterFn) {
@@ -172,6 +171,20 @@ Readability.prototype = {
},
/**
+ * Iterates over a NodeList, and calls _setNodeTag for each node.
+ *
+ * @param NodeList nodeList The nodes to operate on
+ * @param String newTagName the new tag name to use
+ * @return void
+ */
+ _replaceNodeTags: function(nodeList, newTagName) {
+ for (var i = nodeList.length - 1; i >= 0; i--) {
+ var node = nodeList[i];
+ this._setNodeTag(node, newTagName);
+ }
+ },
+
+ /**
* Iterate over a NodeList, which doesn't natively fully implement the Array
* interface.
*
@@ -180,10 +193,9 @@ Readability.prototype = {
*
* @param NodeList nodeList The NodeList.
* @param Function fn The iterate function.
- * @param Boolean backward Whether to use backward iteration.
* @return void
*/
- _forEachNode: function(nodeList, fn, backward) {
+ _forEachNode: function(nodeList, fn) {
Array.prototype.forEach.call(nodeList, fn, this);
},
@@ -228,6 +240,34 @@ Readability.prototype = {
},
/**
+ * Removes the class="" attribute from every element in the given
+ * subtree, except those that match CLASSES_TO_PRESERVE and
+ * the classesToPreserve array from the options object.
+ *
+ * @param Element
+ * @return void
+ */
+ _cleanClasses: function(node) {
+ var classesToPreserve = this._classesToPreserve;
+ var className = (node.getAttribute("class") || "")
+ .split(/\s+/)
+ .filter(function(cls) {
+ return classesToPreserve.indexOf(cls) != -1;
+ })
+ .join(" ");
+
+ if (className) {
+ node.setAttribute("class", className);
+ } else {
+ node.removeAttribute("class");
+ }
+
+ for (node = node.firstElementChild; node; node = node.nextElementSibling) {
+ this._cleanClasses(node);
+ }
+ },
+
+ /**
* Converts each <a> and <img> uri in the given element to an absolute URI,
* ignoring #ref URIs.
*
@@ -307,11 +347,20 @@ Readability.prototype = {
curTitle = origTitle = this._getInnerText(doc.getElementsByTagName('title')[0]);
} catch (e) {/* ignore exceptions setting the title. */}
- if (curTitle.match(/ [\|\-] /)) {
- curTitle = origTitle.replace(/(.*)[\|\-] .*/gi, '$1');
+ var titleHadHierarchicalSeparators = false;
+ function wordCount(str) {
+ return str.split(/\s+/).length;
+ }
+
+ // If there's a separator in the title, first remove the final part
+ if ((/ [\|\-\\\/>»] /).test(curTitle)) {
+ titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle);
+ curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, '$1');
- if (curTitle.split(' ').length < 3)
- curTitle = origTitle.replace(/[^\|\-]*[\|\-](.*)/gi, '$1');
+ // If the resulting title is too short (3 words or fewer), remove
+ // the first part instead:
+ if (wordCount(curTitle) < 3)
+ curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, '$1');
} else if (curTitle.indexOf(': ') !== -1) {
// Check if we have an heading containing this exact string, so we
// could assume it's the full title.
@@ -328,8 +377,13 @@ Readability.prototype = {
curTitle = origTitle.substring(origTitle.lastIndexOf(':') + 1);
// If the title is now too short, try the first colon instead:
- if (curTitle.split(' ').length < 3)
+ if (wordCount(curTitle) < 3) {
curTitle = origTitle.substring(origTitle.indexOf(':') + 1);
+ // But if we have too many words before the colon there's something weird
+ // with the titles and the H tags so let's just use the original title instead
+ } else if (wordCount(origTitle.substr(0, origTitle.indexOf(':'))) > 5) {
+ curTitle = origTitle;
+ }
}
} else if (curTitle.length > 150 || curTitle.length < 15) {
var hOnes = doc.getElementsByTagName('h1');
@@ -339,9 +393,16 @@ Readability.prototype = {
}
curTitle = curTitle.trim();
-
- if (curTitle.split(' ').length <= 4)
+ // If we now have 4 words or fewer as our title, and either no
+ // 'hierarchical' separators (\, /, > or ») were found in the original
+ // title or we decreased the number of words by more than 1 word, use
+ // the original title.
+ var curTitleWordCount = wordCount(curTitle);
+ if (curTitleWordCount <= 4 &&
+ (!titleHadHierarchicalSeparators ||
+ curTitleWordCount != wordCount(origTitle.replace(/[\|\-\\\/>»]+/g, "")) - 1)) {
curTitle = origTitle;
+ }
return curTitle;
},
@@ -362,9 +423,7 @@ Readability.prototype = {
this._replaceBrs(doc.body);
}
- this._forEachNode(doc.getElementsByTagName("font"), function(fontNode) {
- this._setNodeTag(fontNode, "SPAN");
- });
+ this._replaceNodeTags(doc.getElementsByTagName("font"), "SPAN");
},
/**
@@ -464,19 +523,49 @@ Readability.prototype = {
_prepArticle: function(articleContent) {
this._cleanStyles(articleContent);
+ // Check for data tables before we continue, to avoid removing items in
+ // those tables, which will often be isolated even though they're
+ // visually linked to other content-ful elements (text, images, etc.).
+ this._markDataTables(articleContent);
+
// Clean out junk from the article content
this._cleanConditionally(articleContent, "form");
+ this._cleanConditionally(articleContent, "fieldset");
this._clean(articleContent, "object");
this._clean(articleContent, "embed");
this._clean(articleContent, "h1");
this._clean(articleContent, "footer");
- // If there is only one h2, they are probably using it as a header
- // and not a subheader, so remove it since we already have a header.
- if (articleContent.getElementsByTagName('h2').length === 1)
- this._clean(articleContent, "h2");
+ // Clean out elements have "share" in their id/class combinations from final top candidates,
+ // which means we don't remove the top candidates even they have "share".
+ this._forEachNode(articleContent.children, function(topCandidate) {
+ this._cleanMatchedNodes(topCandidate, /share/);
+ });
+
+ // If there is only one h2 and its text content substantially equals article title,
+ // they are probably using it as a header and not a subheader,
+ // so remove it since we already extract the title separately.
+ var h2 = articleContent.getElementsByTagName('h2');
+ if (h2.length === 1) {
+ var lengthSimilarRate = (h2[0].textContent.length - this._articleTitle.length) / this._articleTitle.length;
+ if (Math.abs(lengthSimilarRate) < 0.5) {
+ var titlesMatch = false;
+ if (lengthSimilarRate > 0) {
+ titlesMatch = h2[0].textContent.includes(this._articleTitle);
+ } else {
+ titlesMatch = this._articleTitle.includes(h2[0].textContent);
+ }
+ if (titlesMatch) {
+ this._clean(articleContent, "h2");
+ }
+ }
+ }
this._clean(articleContent, "iframe");
+ this._clean(articleContent, "input");
+ this._clean(articleContent, "textarea");
+ this._clean(articleContent, "select");
+ this._clean(articleContent, "button");
this._cleanHeaders(articleContent);
// Do these last as the previous stuff may have removed junk
@@ -662,9 +751,6 @@ Readability.prototype = {
var pageCacheHtml = page.innerHTML;
- // Check if any "dir" is set on the toplevel document element
- this._articleDir = doc.documentElement.getAttribute("dir");
-
while (true) {
var stripUnlikelyCandidates = this._flagIsActive(this.FLAG_STRIP_UNLIKELYS);
@@ -695,6 +781,15 @@ Readability.prototype = {
}
}
+ // Remove DIV, SECTION, and HEADER nodes without any content(e.g. text, image, video, or iframe).
+ if ((node.tagName === "DIV" || node.tagName === "SECTION" || node.tagName === "HEADER" ||
+ node.tagName === "H1" || node.tagName === "H2" || node.tagName === "H3" ||
+ node.tagName === "H4" || node.tagName === "H5" || node.tagName === "H6") &&
+ this._isElementWithoutContent(node)) {
+ node = this._removeAndGetNext(node);
+ continue;
+ }
+
if (this.DEFAULT_TAGS_TO_SCORE.indexOf(node.tagName) !== -1) {
elementsToScore.push(node);
}
@@ -709,13 +804,14 @@ Readability.prototype = {
var newNode = node.children[0];
node.parentNode.replaceChild(newNode, node);
node = newNode;
+ elementsToScore.push(node);
} else if (!this._hasChildBlockElement(node)) {
node = this._setNodeTag(node, "P");
elementsToScore.push(node);
} else {
// EXPERIMENTAL
this._forEachNode(node.childNodes, function(childNode) {
- if (childNode.nodeType === Node.TEXT_NODE) {
+ if (childNode.nodeType === Node.TEXT_NODE && childNode.textContent.trim().length > 0) {
var p = doc.createElement('p');
p.textContent = childNode.textContent;
p.style.display = 'inline';
@@ -812,6 +908,7 @@ Readability.prototype = {
var topCandidate = topCandidates[0] || null;
var neededToCreateTopCandidate = false;
+ var parentOfTopCandidate;
// If we still have no top candidate, just use the body as a last resort.
// We also have to copy the body node so it is something we can modify.
@@ -831,6 +928,33 @@ Readability.prototype = {
this._initializeNode(topCandidate);
} else if (topCandidate) {
+ // Find a better top candidate node if it contains (at least three) nodes which belong to `topCandidates` array
+ // and whose scores are quite closed with current `topCandidate` node.
+ var alternativeCandidateAncestors = [];
+ for (var i = 1; i < topCandidates.length; i++) {
+ if (topCandidates[i].readability.contentScore / topCandidate.readability.contentScore >= 0.75) {
+ alternativeCandidateAncestors.push(this._getNodeAncestors(topCandidates[i]));
+ }
+ }
+ var MINIMUM_TOPCANDIDATES = 3;
+ if (alternativeCandidateAncestors.length >= MINIMUM_TOPCANDIDATES) {
+ parentOfTopCandidate = topCandidate.parentNode;
+ while (parentOfTopCandidate.tagName !== "BODY") {
+ var listsContainingThisAncestor = 0;
+ for (var ancestorIndex = 0; ancestorIndex < alternativeCandidateAncestors.length && listsContainingThisAncestor < MINIMUM_TOPCANDIDATES; ancestorIndex++) {
+ listsContainingThisAncestor += Number(alternativeCandidateAncestors[ancestorIndex].includes(parentOfTopCandidate));
+ }
+ if (listsContainingThisAncestor >= MINIMUM_TOPCANDIDATES) {
+ topCandidate = parentOfTopCandidate;
+ break;
+ }
+ parentOfTopCandidate = parentOfTopCandidate.parentNode;
+ }
+ }
+ if (!topCandidate.readability) {
+ this._initializeNode(topCandidate);
+ }
+
// Because of our bonus system, parents of candidates might have scores
// themselves. They get half of the node. There won't be nodes with higher
// scores than our topCandidate, but if we see the score going *up* in the first
@@ -838,11 +962,15 @@ Readability.prototype = {
// lurking in other places that we want to unify in. The sibling stuff
// below does some of that - but only if we've looked high enough up the DOM
// tree.
- var parentOfTopCandidate = topCandidate.parentNode;
+ parentOfTopCandidate = topCandidate.parentNode;
var lastScore = topCandidate.readability.contentScore;
// The scores shouldn't get too low.
var scoreThreshold = lastScore / 3;
- while (parentOfTopCandidate && parentOfTopCandidate.readability) {
+ while (parentOfTopCandidate.tagName !== "BODY") {
+ if (!parentOfTopCandidate.readability) {
+ parentOfTopCandidate = parentOfTopCandidate.parentNode;
+ continue;
+ }
var parentScore = parentOfTopCandidate.readability.contentScore;
if (parentScore < scoreThreshold)
break;
@@ -854,6 +982,17 @@ Readability.prototype = {
lastScore = parentOfTopCandidate.readability.contentScore;
parentOfTopCandidate = parentOfTopCandidate.parentNode;
}
+
+ // If the top candidate is the only child, use parent instead. This will help sibling
+ // joining logic when adjacent content is actually located in parent's sibling node.
+ parentOfTopCandidate = topCandidate.parentNode;
+ while (parentOfTopCandidate.tagName != "BODY" && parentOfTopCandidate.children.length == 1) {
+ topCandidate = parentOfTopCandidate;
+ parentOfTopCandidate = topCandidate.parentNode;
+ }
+ if (!topCandidate.readability) {
+ this._initializeNode(topCandidate);
+ }
}
// Now that we have the top candidate, look through its siblings for content
@@ -864,7 +1003,9 @@ Readability.prototype = {
articleContent.id = "readability-content";
var siblingScoreThreshold = Math.max(10, topCandidate.readability.contentScore * 0.2);
- var siblings = topCandidate.parentNode.children;
+ // Keep potential top candidate's parent node to try to get text direction of it later.
+ parentOfTopCandidate = topCandidate.parentNode;
+ var siblings = parentOfTopCandidate.children;
for (var s = 0, sl = siblings.length; s < sl; s++) {
var sibling = siblings[s];
@@ -927,24 +1068,22 @@ Readability.prototype = {
if (this._debug)
this.log("Article content post-prep: " + articleContent.innerHTML);
- if (this._curPageNum === 1) {
- if (neededToCreateTopCandidate) {
- // We already created a fake div thing, and there wouldn't have been any siblings left
- // for the previous loop, so there's no point trying to create a new div, and then
- // move all the children over. Just assign IDs and class names here. No need to append
- // because that already happened anyway.
- topCandidate.id = "readability-page-1";
- topCandidate.className = "page";
- } else {
- var div = doc.createElement("DIV");
- div.id = "readability-page-1";
- div.className = "page";
- var children = articleContent.childNodes;
- while (children.length) {
- div.appendChild(children[0]);
- }
- articleContent.appendChild(div);
+ if (neededToCreateTopCandidate) {
+ // We already created a fake div thing, and there wouldn't have been any siblings left
+ // for the previous loop, so there's no point trying to create a new div, and then
+ // move all the children over. Just assign IDs and class names here. No need to append
+ // because that already happened anyway.
+ topCandidate.id = "readability-page-1";
+ topCandidate.className = "page";
+ } else {
+ var div = doc.createElement("DIV");
+ div.id = "readability-page-1";
+ div.className = "page";
+ var children = articleContent.childNodes;
+ while (children.length) {
+ div.appendChild(children[0]);
}
+ articleContent.appendChild(div);
}
if (this._debug)
@@ -955,7 +1094,7 @@ Readability.prototype = {
// grabArticle with different flags set. This gives us a higher likelihood of
// finding the content, and the sieve approach gives us a higher likelihood of
// finding the -right- content.
- if (this._getInnerText(articleContent, true).length < 500) {
+ if (this._getInnerText(articleContent, true).length < this._wordThreshold) {
page.innerHTML = pageCacheHtml;
if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) {
@@ -968,6 +1107,18 @@ Readability.prototype = {
return null;
}
} else {
+ // Find out text direction from ancestors of final top candidate.
+ var ancestors = [parentOfTopCandidate, topCandidate].concat(this._getNodeAncestors(parentOfTopCandidate));
+ this._someNode(ancestors, function(ancestor) {
+ if (!ancestor.tagName)
+ return false;
+ var articleDir = ancestor.getAttribute("dir");
+ if (articleDir) {
+ this._articleDir = articleDir;
+ return true;
+ }
+ return false;
+ });
return articleContent;
}
}
@@ -1044,12 +1195,15 @@ Readability.prototype = {
metadata.excerpt = values["twitter:description"];
}
- if ("og:title" in values) {
- // Use facebook open graph title.
- metadata.title = values["og:title"];
- } else if ("twitter:title" in values) {
- // Use twitter cards title.
- metadata.title = values["twitter:title"];
+ metadata.title = this._getArticleTitle();
+ if (!metadata.title) {
+ if ("og:title" in values) {
+ // Use facebook open graph title.
+ metadata.title = values["og:title"];
+ } else if ("twitter:title" in values) {
+ // Use twitter cards title.
+ metadata.title = values["twitter:title"];
+ }
}
return metadata;
@@ -1089,6 +1243,13 @@ Readability.prototype = {
});
},
+ _isElementWithoutContent: function(node) {
+ return node.nodeType === Node.ELEMENT_NODE &&
+ node.textContent.trim().length == 0 &&
+ (node.children.length == 0 ||
+ node.children.length == node.getElementsByTagName("br").length + node.getElementsByTagName("hr").length);
+ },
+
/**
* Determine whether element has any children block level elements.
*
@@ -1139,26 +1300,25 @@ Readability.prototype = {
* @return void
**/
_cleanStyles: function(e) {
- e = e || this._doc;
- if (!e)
+ if (!e || e.tagName.toLowerCase() === 'svg')
return;
- var cur = e.firstChild;
- // Remove any root styles, if we're able.
- if (typeof e.removeAttribute === 'function' && e.className !== 'readability-styled')
- e.removeAttribute('style');
-
- // Go until there are no more child nodes
- while (cur !== null) {
- if (cur.nodeType === cur.ELEMENT_NODE) {
- // Remove style attribute(s) :
- if (cur.className !== "readability-styled")
- cur.removeAttribute("style");
+ if (e.className !== 'readability-styled') {
+ // Remove `style` and deprecated presentational attributes
+ for (var i = 0; i < this.PRESENTATIONAL_ATTRIBUTES.length; i++) {
+ e.removeAttribute(this.PRESENTATIONAL_ATTRIBUTES[i]);
+ }
- this._cleanStyles(cur);
+ if (this.DEPRECATED_SIZE_ATTRIBUTE_ELEMS.indexOf(e.tagName) !== -1) {
+ e.removeAttribute('width');
+ e.removeAttribute('height');
}
+ }
- cur = cur.nextSibling;
+ var cur = e.firstElementChild;
+ while (cur !== null) {
+ this._cleanStyles(cur);
+ cur = cur.nextElementSibling;
}
},
@@ -1185,368 +1345,6 @@ Readability.prototype = {
},
/**
- * Find a cleaned up version of the current URL, to use for comparing links for possible next-pageyness.
- *
- * @author Dan Lacy
- * @return string the base url
- **/
- _findBaseUrl: function() {
- var uri = this._uri;
- var noUrlParams = uri.path.split("?")[0];
- var urlSlashes = noUrlParams.split("/").reverse();
- var cleanedSegments = [];
- var possibleType = "";
-
- for (var i = 0, slashLen = urlSlashes.length; i < slashLen; i += 1) {
- var segment = urlSlashes[i];
-
- // Split off and save anything that looks like a file type.
- if (segment.indexOf(".") !== -1) {
- possibleType = segment.split(".")[1];
-
- // If the type isn't alpha-only, it's probably not actually a file extension.
- if (!possibleType.match(/[^a-zA-Z]/))
- segment = segment.split(".")[0];
- }
-
- // EW-CMS specific segment replacement. Ugly.
- // Example: http://www.ew.com/ew/article/0,,20313460_20369436,00.html
- if (segment.indexOf(',00') !== -1)
- segment = segment.replace(',00', '');
-
- // If our first or second segment has anything looking like a page number, remove it.
- if (segment.match(/((_|-)?p[a-z]*|(_|-))[0-9]{1,2}$/i) && ((i === 1) || (i === 0)))
- segment = segment.replace(/((_|-)?p[a-z]*|(_|-))[0-9]{1,2}$/i, "");
-
- var del = false;
-
- // If this is purely a number, and it's the first or second segment,
- // it's probably a page number. Remove it.
- if (i < 2 && segment.match(/^\d{1,2}$/))
- del = true;
-
- // If this is the first segment and it's just "index", remove it.
- if (i === 0 && segment.toLowerCase() === "index")
- del = true;
-
- // If our first or second segment is smaller than 3 characters,
- // and the first segment was purely alphas, remove it.
- if (i < 2 && segment.length < 3 && !urlSlashes[0].match(/[a-z]/i))
- del = true;
-
- // If it's not marked for deletion, push it to cleanedSegments.
- if (!del)
- cleanedSegments.push(segment);
- }
-
- // This is our final, cleaned, base article URL.
- return uri.scheme + "://" + uri.host + cleanedSegments.reverse().join("/");
- },
-
- /**
- * Look for any paging links that may occur within the document.
- *
- * @param body
- * @return object (array)
- **/
- _findNextPageLink: function(elem) {
- var uri = this._uri;
- var possiblePages = {};
- var allLinks = elem.getElementsByTagName('a');
- var articleBaseUrl = this._findBaseUrl();
-
- // Loop through all links, looking for hints that they may be next-page links.
- // Things like having "page" in their textContent, className or id, or being a child
- // of a node with a page-y className or id.
- //
- // Also possible: levenshtein distance? longest common subsequence?
- //
- // After we do that, assign each page a score, and
- for (var i = 0, il = allLinks.length; i < il; i += 1) {
- var link = allLinks[i];
- var linkHref = allLinks[i].href.replace(/#.*$/, '').replace(/\/$/, '');
-
- // If we've already seen this page, ignore it.
- if (linkHref === "" ||
- linkHref === articleBaseUrl ||
- linkHref === uri.spec ||
- linkHref in this._parsedPages) {
- continue;
- }
-
- // If it's on a different domain, skip it.
- if (uri.host !== linkHref.split(/\/+/g)[1])
- continue;
-
- var linkText = this._getInnerText(link);
-
- // If the linkText looks like it's not the next page, skip it.
- if (linkText.match(this.REGEXPS.extraneous) || linkText.length > 25)
- continue;
-
- // If the leftovers of the URL after removing the base URL don't contain
- // any digits, it's certainly not a next page link.
- var linkHrefLeftover = linkHref.replace(articleBaseUrl, '');
- if (!linkHrefLeftover.match(/\d/))
- continue;
-
- if (!(linkHref in possiblePages)) {
- possiblePages[linkHref] = {"score": 0, "linkText": linkText, "href": linkHref};
- } else {
- possiblePages[linkHref].linkText += ' | ' + linkText;
- }
-
- var linkObj = possiblePages[linkHref];
-
- // If the articleBaseUrl isn't part of this URL, penalize this link. It could
- // still be the link, but the odds are lower.
- // Example: http://www.actionscript.org/resources/articles/745/1/JavaScript-and-VBScript-Injection-in-ActionScript-3/Page1.html
- if (linkHref.indexOf(articleBaseUrl) !== 0)
- linkObj.score -= 25;
-
- var linkData = linkText + ' ' + link.className + ' ' + link.id;
- if (linkData.match(this.REGEXPS.nextLink))
- linkObj.score += 50;
-
- if (linkData.match(/pag(e|ing|inat)/i))
- linkObj.score += 25;
-
- if (linkData.match(/(first|last)/i)) {
- // -65 is enough to negate any bonuses gotten from a > or » in the text,
- // If we already matched on "next", last is probably fine.
- // If we didn't, then it's bad. Penalize.
- if (!linkObj.linkText.match(this.REGEXPS.nextLink))
- linkObj.score -= 65;
- }
-
- if (linkData.match(this.REGEXPS.negative) || linkData.match(this.REGEXPS.extraneous))
- linkObj.score -= 50;
-
- if (linkData.match(this.REGEXPS.prevLink))
- linkObj.score -= 200;
-
- // If a parentNode contains page or paging or paginat
- var parentNode = link.parentNode;
- var positiveNodeMatch = false;
- var negativeNodeMatch = false;
-
- while (parentNode) {
- var parentNodeClassAndId = parentNode.className + ' ' + parentNode.id;
-
- if (!positiveNodeMatch && parentNodeClassAndId && parentNodeClassAndId.match(/pag(e|ing|inat)/i)) {
- positiveNodeMatch = true;
- linkObj.score += 25;
- }
-
- if (!negativeNodeMatch && parentNodeClassAndId && parentNodeClassAndId.match(this.REGEXPS.negative)) {
- // If this is just something like "footer", give it a negative.
- // If it's something like "body-and-footer", leave it be.
- if (!parentNodeClassAndId.match(this.REGEXPS.positive)) {
- linkObj.score -= 25;
- negativeNodeMatch = true;
- }
- }
-
- parentNode = parentNode.parentNode;
- }
-
- // If the URL looks like it has paging in it, add to the score.
- // Things like /page/2/, /pagenum/2, ?p=3, ?page=11, ?pagination=34
- if (linkHref.match(/p(a|g|ag)?(e|ing|ination)?(=|\/)[0-9]{1,2}/i) || linkHref.match(/(page|paging)/i))
- linkObj.score += 25;
-
- // If the URL contains negative values, give a slight decrease.
- if (linkHref.match(this.REGEXPS.extraneous))
- linkObj.score -= 15;
-
- /**
- * Minor punishment to anything that doesn't match our current URL.
- * NOTE: I'm finding this to cause more harm than good where something is exactly 50 points.
- * Dan, can you show me a counterexample where this is necessary?
- * if (linkHref.indexOf(window.location.href) !== 0) {
- * linkObj.score -= 1;
- * }
- **/
-
- // If the link text can be parsed as a number, give it a minor bonus, with a slight
- // bias towards lower numbered pages. This is so that pages that might not have 'next'
- // in their text can still get scored, and sorted properly by score.
- var linkTextAsNumber = parseInt(linkText, 10);
- if (linkTextAsNumber) {
- // Punish 1 since we're either already there, or it's probably
- // before what we want anyways.
- if (linkTextAsNumber === 1) {
- linkObj.score -= 10;
- } else {
- linkObj.score += Math.max(0, 10 - linkTextAsNumber);
- }
- }
- }
-
- // Loop thrugh all of our possible pages from above and find our top
- // candidate for the next page URL. Require at least a score of 50, which
- // is a relatively high confidence that this page is the next link.
- var topPage = null;
- for (var page in possiblePages) {
- if (possiblePages.hasOwnProperty(page)) {
- if (possiblePages[page].score >= 50 &&
- (!topPage || topPage.score < possiblePages[page].score))
- topPage = possiblePages[page];
- }
- }
-
- var nextHref = null;
- if (topPage) {
- nextHref = topPage.href.replace(/\/$/, '');
-
- this.log('NEXT PAGE IS ' + nextHref);
- this._parsedPages[nextHref] = true;
- }
- return nextHref;
- },
-
- _successfulRequest: function(request) {
- return (request.status >= 200 && request.status < 300) ||
- request.status === 304 ||
- (request.status === 0 && request.responseText);
- },
-
- _ajax: function(url, options) {
- var request = new XMLHttpRequest();
-
- function respondToReadyState(readyState) {
- if (request.readyState === 4) {
- if (this._successfulRequest(request)) {
- if (options.success)
- options.success(request);
- } else if (options.error) {
- options.error(request);
- }
- }
- }
-
- if (typeof options === 'undefined')
- options = {};
-
- request.onreadystatechange = respondToReadyState;
-
- request.open('get', url, true);
- request.setRequestHeader('Accept', 'text/html');
-
- try {
- request.send(options.postBody);
- } catch (e) {
- if (options.error)
- options.error();
- }
-
- return request;
- },
-
- _appendNextPage: function(nextPageLink) {
- var doc = this._doc;
- this._curPageNum += 1;
-
- var articlePage = doc.createElement("DIV");
- articlePage.id = 'readability-page-' + this._curPageNum;
- articlePage.className = 'page';
- articlePage.innerHTML = '<p class="page-separator" title="Page ' + this._curPageNum + '">&sect;</p>';
-
- doc.getElementById("readability-content").appendChild(articlePage);
-
- if (this._curPageNum > this._maxPages) {
- var nextPageMarkup = "<div style='text-align: center'><a href='" + nextPageLink + "'>View Next Page</a></div>";
- articlePage.innerHTML = articlePage.innerHTML + nextPageMarkup;
- return;
- }
-
- // Now that we've built the article page DOM element, get the page content
- // asynchronously and load the cleaned content into the div we created for it.
- (function(pageUrl, thisPage) {
- this._ajax(pageUrl, {
- success: function(r) {
-
- // First, check to see if we have a matching ETag in headers - if we do, this is a duplicate page.
- var eTag = r.getResponseHeader('ETag');
- if (eTag) {
- if (eTag in this._pageETags) {
- this.log("Exact duplicate page found via ETag. Aborting.");
- articlePage.style.display = 'none';
- return;
- }
- this._pageETags[eTag] = 1;
- }
-
- // TODO: this ends up doubling up page numbers on NYTimes articles. Need to generically parse those away.
- var page = doc.createElement("DIV");
-
- // Do some preprocessing to our HTML to make it ready for appending.
- // - Remove any script tags. Swap and reswap newlines with a unicode
- // character because multiline regex doesn't work in javascript.
- // - Turn any noscript tags into divs so that we can parse them. This
- // allows us to find any next page links hidden via javascript.
- // - Turn all double br's into p's - was handled by prepDocument in the original view.
- // Maybe in the future abstract out prepDocument to work for both the original document
- // and AJAX-added pages.
- var responseHtml = r.responseText.replace(/\n/g, '\uffff').replace(/<script.*?>.*?<\/script>/gi, '');
- responseHtml = responseHtml.replace(/\n/g, '\uffff').replace(/<script.*?>.*?<\/script>/gi, '');
- responseHtml = responseHtml.replace(/\uffff/g, '\n').replace(/<(\/?)noscript/gi, '<$1div');
- responseHtml = responseHtml.replace(this.REGEXPS.replaceFonts, '<$1span>');
-
- page.innerHTML = responseHtml;
- this._replaceBrs(page);
-
- // Reset all flags for the next page, as they will search through it and
- // disable as necessary at the end of grabArticle.
- this._flags = 0x1 | 0x2 | 0x4;
-
- var secondNextPageLink = this._findNextPageLink(page);
-
- // NOTE: if we end up supporting _appendNextPage(), we'll need to
- // change this call to be async
- var content = this._grabArticle(page);
-
- if (!content) {
- this.log("No content found in page to append. Aborting.");
- return;
- }
-
- // Anti-duplicate mechanism. Essentially, get the first paragraph of our new page.
- // Compare it against all of the the previous document's we've gotten. If the previous
- // document contains exactly the innerHTML of this first paragraph, it's probably a duplicate.
- var firstP = content.getElementsByTagName("P").length ? content.getElementsByTagName("P")[0] : null;
- if (firstP && firstP.innerHTML.length > 100) {
- for (var i = 1; i <= this._curPageNum; i += 1) {
- var rPage = doc.getElementById('readability-page-' + i);
- if (rPage && rPage.innerHTML.indexOf(firstP.innerHTML) !== -1) {
- this.log('Duplicate of page ' + i + ' - skipping.');
- articlePage.style.display = 'none';
- this._parsedPages[pageUrl] = true;
- return;
- }
- }
- }
-
- this._removeScripts(content);
-
- thisPage.innerHTML = thisPage.innerHTML + content.innerHTML;
-
- // After the page has rendered, post process the content. This delay is necessary because,
- // in webkit at least, offsetWidth is not set in time to determine image width. We have to
- // wait a little bit for reflow to finish before we can fix floating images.
- setTimeout((function() {
- this._postProcessContent(thisPage);
- }).bind(this), 500);
-
-
- if (secondNextPageLink)
- this._appendNextPage(secondNextPageLink);
- }
- });
- }).bind(this)(nextPageLink, articlePage);
- },
-
- /**
* Get an elements class/id weight. Uses regular expressions to tell if this
* element looks good or bad.
*
@@ -1617,16 +1415,17 @@ Readability.prototype = {
* @param HTMLElement node
* @param String tagName
* @param Number maxDepth
+ * @param Function filterFn a filter to invoke to determine whether this node 'counts'
* @return Boolean
*/
- _hasAncestorTag: function(node, tagName, maxDepth) {
+ _hasAncestorTag: function(node, tagName, maxDepth, filterFn) {
maxDepth = maxDepth || 3;
tagName = tagName.toUpperCase();
var depth = 0;
while (node.parentNode) {
- if (depth > maxDepth)
+ if (maxDepth > 0 && depth > maxDepth)
return false;
- if (node.parentNode.tagName === tagName)
+ if (node.parentNode.tagName === tagName && (!filterFn || filterFn(node.parentNode)))
return true;
node = node.parentNode;
depth++;
@@ -1635,6 +1434,93 @@ Readability.prototype = {
},
/**
+ * Return an object indicating how many rows and columns this table has.
+ */
+ _getRowAndColumnCount: function(table) {
+ var rows = 0;
+ var columns = 0;
+ var trs = table.getElementsByTagName("tr");
+ for (var i = 0; i < trs.length; i++) {
+ var rowspan = trs[i].getAttribute("rowspan") || 0;
+ if (rowspan) {
+ rowspan = parseInt(rowspan, 10);
+ }
+ rows += (rowspan || 1);
+
+ // Now look for column-related info
+ var columnsInThisRow = 0;
+ var cells = trs[i].getElementsByTagName("td");
+ for (var j = 0; j < cells.length; j++) {
+ var colspan = cells[j].getAttribute("colspan") || 0;
+ if (colspan) {
+ colspan = parseInt(colspan, 10);
+ }
+ columnsInThisRow += (colspan || 1);
+ }
+ columns = Math.max(columns, columnsInThisRow);
+ }
+ return {rows: rows, columns: columns};
+ },
+
+ /**
+ * Look for 'data' (as opposed to 'layout') tables, for which we use
+ * similar checks as
+ * https://dxr.mozilla.org/mozilla-central/rev/71224049c0b52ab190564d3ea0eab089a159a4cf/accessible/html/HTMLTableAccessible.cpp#920
+ */
+ _markDataTables: function(root) {
+ var tables = root.getElementsByTagName("table");
+ for (var i = 0; i < tables.length; i++) {
+ var table = tables[i];
+ var role = table.getAttribute("role");
+ if (role == "presentation") {
+ table._readabilityDataTable = false;
+ continue;
+ }
+ var datatable = table.getAttribute("datatable");
+ if (datatable == "0") {
+ table._readabilityDataTable = false;
+ continue;
+ }
+ var summary = table.getAttribute("summary");
+ if (summary) {
+ table._readabilityDataTable = true;
+ continue;
+ }
+
+ var caption = table.getElementsByTagName("caption")[0];
+ if (caption && caption.childNodes.length > 0) {
+ table._readabilityDataTable = true;
+ continue;
+ }
+
+ // If the table has a descendant with any of these tags, consider a data table:
+ var dataTableDescendants = ["col", "colgroup", "tfoot", "thead", "th"];
+ var descendantExists = function(tag) {
+ return !!table.getElementsByTagName(tag)[0];
+ };
+ if (dataTableDescendants.some(descendantExists)) {
+ this.log("Data table because found data-y descendant");
+ table._readabilityDataTable = true;
+ continue;
+ }
+
+ // Nested tables indicate a layout table:
+ if (table.getElementsByTagName("table")[0]) {
+ table._readabilityDataTable = false;
+ continue;
+ }
+
+ var sizeInfo = this._getRowAndColumnCount(table);
+ if (sizeInfo.rows >= 10 || sizeInfo.columns > 4) {
+ table._readabilityDataTable = true;
+ continue;
+ }
+ // Now just go by size entirely:
+ table._readabilityDataTable = sizeInfo.rows * sizeInfo.columns > 10;
+ }
+ },
+
+ /**
* Clean an element of all tags of type "tag" if they look fishy.
* "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc.
*
@@ -1652,6 +1538,15 @@ Readability.prototype = {
//
// TODO: Consider taking into account original contentScore here.
this._removeNodes(e.getElementsByTagName(tag), function(node) {
+ // First check if we're in a data table, in which case don't remove us.
+ var isDataTable = function(t) {
+ return t._readabilityDataTable;
+ };
+
+ if (this._hasAncestorTag(node, "table", -1, isDataTable)) {
+ return false;
+ }
+
var weight = this._getClassWeight(node);
var contentScore = 0;
@@ -1667,7 +1562,7 @@ Readability.prototype = {
// ominous signs, remove the element.
var p = node.getElementsByTagName("p").length;
var img = node.getElementsByTagName("img").length;
- var li = node.getElementsByTagName("li").length-100;
+ var li = node.getElementsByTagName("li").length - 100;
var input = node.getElementsByTagName("input").length;
var embedCount = 0;
@@ -1681,11 +1576,10 @@ Readability.prototype = {
var contentLength = this._getInnerText(node).length;
var haveToRemove =
- // Make an exception for elements with no p's and exactly 1 img.
- (img > p && !this._hasAncestorTag(node, "figure")) ||
+ (img > 1 && p / img < 0.5 && !this._hasAncestorTag(node, "figure")) ||
(!isList && li > p) ||
(input > Math.floor(p/3)) ||
- (!isList && contentLength < 25 && (img === 0 || img > 2)) ||
+ (!isList && contentLength < 25 && (img === 0 || img > 2) && !this._hasAncestorTag(node, "figure")) ||
(!isList && weight < 25 && linkDensity > 0.2) ||
(weight >= 25 && linkDensity > 0.5) ||
((embedCount === 1 && contentLength < 75) || embedCount > 1);
@@ -1696,6 +1590,25 @@ Readability.prototype = {
},
/**
+ * Clean out elements whose id/class combinations match specific string.
+ *
+ * @param Element
+ * @param RegExp match id/class combination.
+ * @return void
+ **/
+ _cleanMatchedNodes: function(e, regex) {
+ var endOfSearchMarkerNode = this._getNextNode(e, true);
+ var next = this._getNextNode(e);
+ while (next && next != endOfSearchMarkerNode) {
+ if (regex.test(next.className + " " + next.id)) {
+ next = this._removeAndGetNext(next);
+ } else {
+ next = this._getNextNode(next);
+ }
+ }
+ },
+
+ /**
* Clean out spurious headers from an Element. Checks things like classnames and link density.
*
* @param Element
@@ -1713,10 +1626,6 @@ Readability.prototype = {
return (this._flags & flag) > 0;
},
- _addFlag: function(flag) {
- this._flags = this._flags | flag;
- },
-
_removeFlag: function(flag) {
this._flags = this._flags & ~flag;
},
@@ -1807,20 +1716,10 @@ Readability.prototype = {
// Remove script tags from the document.
this._removeScripts(this._doc);
- // FIXME: Disabled multi-page article support for now as it
- // needs more work on infrastructure.
-
- // Make sure this document is added to the list of parsed pages first,
- // so we don't double up on the first page.
- // this._parsedPages[uri.spec.replace(/\/$/, '')] = true;
-
- // Pull out any possible next page link first.
- // var nextPageLink = this._findNextPageLink(doc.body);
-
this._prepDocument();
var metadata = this._getArticleMetadata();
- var articleTitle = metadata.title || this._getArticleTitle();
+ this._articleTitle = metadata.title;
var articleContent = this._grabArticle();
if (!articleContent)
@@ -1830,14 +1729,6 @@ Readability.prototype = {
this._postProcessContent(articleContent);
- // if (nextPageLink) {
- // // Append any additional pages after a small timeout so that people
- // // can start reading without having to wait for this to finish processing.
- // setTimeout((function() {
- // this._appendNextPage(nextPageLink);
- // }).bind(this), 500);
- // }
-
// If we haven't found an excerpt in the article's metadata, use the article's
// first paragraph as the excerpt. This is used for displaying a preview of
// the article's content.
@@ -1851,7 +1742,7 @@ Readability.prototype = {
var textContent = articleContent.textContent;
return {
uri: this._uri,
- title: articleTitle,
+ title: this._articleTitle,
byline: metadata.byline || this._articleByline,
dir: this._articleDir,
content: articleContent.innerHTML,
@@ -1861,3 +1752,7 @@ Readability.prototype = {
};
}
};
+
+if (typeof module === "object") {
+ module.exports = Readability;
+}
diff --git a/toolkit/components/reader/ReaderMode.jsm b/toolkit/components/reader/ReaderMode.jsm
index 033a02489..e9eb83154 100644
--- a/toolkit/components/reader/ReaderMode.jsm
+++ b/toolkit/components/reader/ReaderMode.jsm
@@ -8,15 +8,18 @@ this.EXPORTED_SYMBOLS = ["ReaderMode"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-// Constants for telemetry.
-const DOWNLOAD_SUCCESS = 0;
-const DOWNLOAD_ERROR_XHR = 1;
-const DOWNLOAD_ERROR_NO_DOC = 2;
-
-const PARSE_SUCCESS = 0;
-const PARSE_ERROR_TOO_MANY_ELEMENTS = 1;
-const PARSE_ERROR_WORKER = 2;
-const PARSE_ERROR_NO_ARTICLE = 3;
+// Class names to preserve in the readerized output. We preserve these class
+// names so that rules in aboutReader.css can match them.
+const CLASSES_TO_PRESERVE = [
+ "caption",
+ "hidden",
+ "invisble",
+ "sr-only",
+ "visually-hidden",
+ "visuallyhidden",
+ "wp-caption",
+ "wp-caption-text",
+];
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -24,17 +27,15 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.importGlobalProperties(["XMLHttpRequest"]);
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js");
-XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher", "resource://gre/modules/Messaging.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderWorker", "resource://gre/modules/reader/ReaderWorker.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
XPCOMUtils.defineLazyGetter(this, "Readability", function() {
let scope = {};
scope.dump = this.dump;
Services.scriptloader.loadSubScript("resource://gre/modules/reader/Readability.js", scope);
- return scope["Readability"];
+ return scope.Readability;
});
this.ReaderMode = {
@@ -61,21 +62,13 @@ this.ReaderMode = {
return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
},
- get isOnLowMemoryPlatform() {
- let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
- delete this.isOnLowMemoryPlatform;
- return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
- },
-
- _getStateForParseOnLoad: function () {
+ _getStateForParseOnLoad() {
let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled");
- // For low-memory devices, don't allow reader mode since it takes up a lot of memory.
- // See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
- return isForceEnabled || (isEnabled && !this.isOnLowMemoryPlatform);
+ return isForceEnabled || isEnabled;
},
- observe: function(aMessage, aTopic, aData) {
+ observe(aMessage, aTopic, aData) {
switch (aTopic) {
case "nsPref:changed":
if (aData.startsWith("reader.parse-on-load.")) {
@@ -91,7 +84,7 @@ this.ReaderMode = {
* Enter the reader mode by going forward one step in history if applicable,
* if not, append the about:reader page in the history instead.
*/
- enterReaderMode: function(docShell, win) {
+ enterReaderMode(docShell, win) {
let url = win.document.location.href;
let readerURL = "about:reader?url=" + encodeURIComponent(url);
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
@@ -112,7 +105,7 @@ this.ReaderMode = {
* Exit the reader mode by going back one step in history if applicable,
* if not, append the original page in the history instead.
*/
- leaveReaderMode: function(docShell, win) {
+ leaveReaderMode(docShell, win) {
let url = win.document.location.href;
let originalURL = this.getOriginalUrl(url);
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
@@ -136,14 +129,14 @@ this.ReaderMode = {
* @return The original URL for the article, or null if we did not find
* a properly formatted about:reader URL.
*/
- getOriginalUrl: function(url) {
+ getOriginalUrl(url) {
if (!url.startsWith("about:reader?")) {
return null;
}
let outerHash = "";
try {
- let uriObj = Services.io.newURI(url, null, null);
+ let uriObj = Services.io.newURI(url);
url = uriObj.specIgnoringRef;
outerHash = uriObj.ref;
} catch (ex) { /* ignore, use the raw string */ }
@@ -155,27 +148,45 @@ this.ReaderMode = {
let originalUrl = searchParams.get("url");
if (outerHash) {
try {
- let uriObj = Services.io.newURI(originalUrl, null, null);
- uriObj = Services.io.newURI('#' + outerHash, null, uriObj);
+ let uriObj = Services.io.newURI(originalUrl);
+ uriObj = Services.io.newURI("#" + outerHash, null, uriObj);
originalUrl = uriObj.spec;
} catch (ex) {}
}
return originalUrl;
},
+ getOriginalUrlObjectForDisplay(url) {
+ let originalUrl = this.getOriginalUrl(url);
+ if (originalUrl) {
+ let uriObj;
+ try {
+ uriObj = Services.uriFixup.createFixupURI(originalUrl, Services.uriFixup.FIXUP_FLAG_NONE);
+ } catch (ex) {
+ return null;
+ }
+ try {
+ return Services.uriFixup.createExposableURI(uriObj);
+ } catch (ex) {
+ return null;
+ }
+ }
+ return null;
+ },
+
/**
* Decides whether or not a document is reader-able without parsing the whole thing.
*
* @param doc A document to parse.
* @return boolean Whether or not we should show the reader mode button.
*/
- isProbablyReaderable: function(doc) {
+ isProbablyReaderable(doc) {
// Only care about 'real' HTML documents:
if (doc.mozSyntheticDocument || !(doc instanceof doc.defaultView.HTMLDocument)) {
return false;
}
- let uri = Services.io.newURI(doc.location.href, null, null);
+ let uri = Services.io.newURI(doc.location.href);
if (!this._shouldCheckUri(uri)) {
return false;
}
@@ -187,12 +198,12 @@ this.ReaderMode = {
return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils));
},
- isNodeVisible: function(utils, node) {
+ isNodeVisible(utils, node) {
let bounds = utils.getBoundsWithoutFlushing(node);
return bounds.height > 0 && bounds.width > 0;
},
- getUtilsForWin: function(win) {
+ getUtilsForWin(win) {
return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
},
@@ -204,16 +215,14 @@ this.ReaderMode = {
* @return {Promise}
* @resolves JS object representing the article, or null if no article is found.
*/
- parseDocument: Task.async(function* (doc) {
- let documentURI = Services.io.newURI(doc.documentURI, null, null);
- let baseURI = Services.io.newURI(doc.baseURI, null, null);
- if (!this._shouldCheckUri(documentURI) || !this._shouldCheckUri(baseURI, true)) {
+ parseDocument(doc) {
+ if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
this.log("Reader mode disabled for URI");
return null;
}
- return yield this._readerParse(baseURI, doc);
- }),
+ return this._readerParse(doc);
+ },
/**
* Downloads and parses a document from a URL.
@@ -222,19 +231,28 @@ this.ReaderMode = {
* @return {Promise}
* @resolves JS object representing the article, or null if no article is found.
*/
- downloadAndParseDocument: Task.async(function* (url) {
- let doc = yield this._downloadDocument(url);
- let uri = Services.io.newURI(doc.baseURI, null, null);
- if (!this._shouldCheckUri(uri, true)) {
+ async downloadAndParseDocument(url) {
+ let doc = await this._downloadDocument(url);
+ if (!doc) {
+ return null;
+ }
+ if (!this._shouldCheckUri(doc.documentURIObject) || !this._shouldCheckUri(doc.baseURIObject, true)) {
this.log("Reader mode disabled for URI");
return null;
}
- return yield this._readerParse(uri, doc);
- }),
+ return await this._readerParse(doc);
+ },
- _downloadDocument: function (url) {
- let histogram = Services.telemetry.getHistogramById("READER_MODE_DOWNLOAD_RESULT");
+ _downloadDocument(url) {
+ try {
+ if (!this._shouldCheckUri(Services.io.newURI(url))) {
+ return null;
+ }
+ } catch (ex) {
+ Cu.reportError(new Error(`Couldn't create URI from ${url} to download: ${ex}`));
+ return null;
+ }
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
@@ -243,14 +261,12 @@ this.ReaderMode = {
xhr.onload = evt => {
if (xhr.status !== 200) {
reject("Reader mode XHR failed with status: " + xhr.status);
- histogram.add(DOWNLOAD_ERROR_XHR);
return;
}
let doc = xhr.responseXML;
if (!doc) {
reject("Reader mode XHR didn't return a document");
- histogram.add(DOWNLOAD_ERROR_NO_DOC);
return;
}
@@ -261,7 +277,7 @@ this.ReaderMode = {
if (content) {
let urlIndex = content.toUpperCase().indexOf("URL=");
if (urlIndex > -1) {
- let baseURI = Services.io.newURI(url, null, null);
+ let baseURI = Services.io.newURI(url);
let newURI = Services.io.newURI(content.substring(urlIndex + 4), null, baseURI);
let newURL = newURI.spec;
let ssm = Services.scriptSecurityManager;
@@ -290,10 +306,10 @@ this.ReaderMode = {
// Convert these to real URIs to make sure the escaping (or lack
// thereof) is identical:
try {
- responseURL = Services.io.newURI(responseURL, null, null).specIgnoringRef;
+ responseURL = Services.io.newURI(responseURL).specIgnoringRef;
} catch (ex) { /* Ignore errors - we'll use what we had before */ }
try {
- givenURL = Services.io.newURI(givenURL, null, null).specIgnoringRef;
+ givenURL = Services.io.newURI(givenURL).specIgnoringRef;
} catch (ex) { /* Ignore errors - we'll use what we had before */ }
if (responseURL != givenURL) {
@@ -303,7 +319,6 @@ this.ReaderMode = {
return;
}
resolve(doc);
- histogram.add(DOWNLOAD_SUCCESS);
};
xhr.send();
});
@@ -318,17 +333,17 @@ this.ReaderMode = {
* @resolves JS object representing the article, or null if no article is found.
* @rejects OS.File.Error
*/
- getArticleFromCache: Task.async(function* (url) {
+ async getArticleFromCache(url) {
let path = this._toHashedPath(url);
try {
- let array = yield OS.File.read(path);
+ let array = await OS.File.read(path);
return JSON.parse(new TextDecoder().decode(array));
} catch (e) {
if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile)
throw e;
return null;
}
- }),
+ },
/**
* Stores an article in the cache.
@@ -338,14 +353,14 @@ this.ReaderMode = {
* @resolves When the article is stored.
* @rejects OS.File.Error
*/
- storeArticleInCache: Task.async(function* (article) {
+ async storeArticleInCache(article) {
let array = new TextEncoder().encode(JSON.stringify(article));
let path = this._toHashedPath(article.url);
- yield this._ensureCacheDir();
+ await this._ensureCacheDir();
return OS.File.writeAtomic(path, array, { tmpPath: path + ".tmp" })
.then(success => {
OS.File.stat(path).then(info => {
- return Messaging.sendRequest({
+ return EventDispatcher.instance.sendRequest({
type: "Reader:AddedToCache",
url: article.url,
size: info.size,
@@ -353,7 +368,7 @@ this.ReaderMode = {
});
});
});
- }),
+ },
/**
* Removes an article from the cache given an article URI.
@@ -363,26 +378,29 @@ this.ReaderMode = {
* @resolves When the article is removed.
* @rejects OS.File.Error
*/
- removeArticleFromCache: Task.async(function* (url) {
+ async removeArticleFromCache(url) {
let path = this._toHashedPath(url);
- yield OS.File.remove(path);
- }),
+ await OS.File.remove(path);
+ },
- log: function(msg) {
+ log(msg) {
if (this.DEBUG)
dump("Reader: " + msg);
},
_blockedHosts: [
- "mail.google.com",
+ "amazon.com",
+ "basilisk-browser.org",
"github.com",
+ "mail.google.com",
+ "palemoon.org",
"pinterest.com",
"reddit.com",
"twitter.com",
"youtube.com",
],
- _shouldCheckUri: function (uri, isBaseUri = false) {
+ _shouldCheckUri(uri, isBaseUri = false) {
if (!(uri.schemeIs("http") || uri.schemeIs("https"))) {
this.log("Not parsing URI scheme: " + uri.scheme);
return false;
@@ -412,59 +430,77 @@ this.ReaderMode = {
* Attempts to parse a document into an article. Heavy lifting happens
* in readerWorker.js.
*
- * @param uri The base URI of the article.
* @param doc The document to parse.
* @return {Promise}
* @resolves JS object representing the article, or null if no article is found.
*/
- _readerParse: Task.async(function* (uri, doc) {
- let histogram = Services.telemetry.getHistogramById("READER_MODE_PARSE_RESULT");
+ async _readerParse(doc) {
if (this.parseNodeLimit) {
let numTags = doc.getElementsByTagName("*").length;
if (numTags > this.parseNodeLimit) {
- this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found");
- histogram.add(PARSE_ERROR_TOO_MANY_ELEMENTS);
+ this.log("Aborting parse for " + doc.baseURIObject.spec + "; " + numTags + " elements found");
return null;
}
}
+ // Fetch this here before we send `doc` off to the worker thread, as later on the
+ // document might be nuked but we will still want the URI.
+ let {documentURI} = doc;
+
let uriParam = {
- spec: uri.spec,
- host: uri.host,
- prePath: uri.prePath,
- scheme: uri.scheme,
- pathBase: Services.io.newURI(".", null, uri).spec
+ spec: doc.baseURIObject.spec,
+ host: doc.baseURIObject.host,
+ prePath: doc.baseURIObject.prePath,
+ scheme: doc.baseURIObject.scheme,
+ pathBase: Services.io.newURI(".", null, doc.baseURIObject).spec
+ };
+
+ let langAttributes = {
+ charset: doc.characterSet,
+ lang: doc.documentElement.lang
};
let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
createInstance(Ci.nsIDOMSerializer);
let serializedDoc = serializer.serializeToString(doc);
+ let options = {
+ classesToPreserve: CLASSES_TO_PRESERVE,
+ };
+
let article = null;
try {
- article = yield ReaderWorker.post("parseDocument", [uriParam, serializedDoc]);
+ article = await ReaderWorker.post("parseDocument", [uriParam, serializedDoc, options]);
} catch (e) {
Cu.reportError("Error in ReaderWorker: " + e);
- histogram.add(PARSE_ERROR_WORKER);
}
+ // Explicitly null out doc to make it clear it might not be available from this
+ // point on.
+ doc = null;
+
if (!article) {
this.log("Worker did not return an article");
- histogram.add(PARSE_ERROR_NO_ARTICLE);
return null;
}
- // Readability returns a URI object, but we only care about the URL.
- article.url = article.uri.spec;
+ // Readability returns a URI object based on the baseURI, but we only care
+ // about the original document's URL from now on. This also avoids spoofing
+ // attempts where the baseURI doesn't match the domain of the documentURI
+ article.url = documentURI;
delete article.uri;
let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
.convertToPlainText(article.title, flags, 0);
- histogram.add(PARSE_SUCCESS);
+ await this._assignLanguage(article, langAttributes);
+ this._maybeAssignTextDirection(article);
+
+ this._assignReadTime(article);
+
return article;
- }),
+ },
get _cryptoHash() {
delete this._cryptoHash;
@@ -485,7 +521,7 @@ this.ReaderMode = {
* @param url The article URL. This should have referrers removed.
* @return The file path to the cached article.
*/
- _toHashedPath: function (url) {
+ _toHashedPath(url) {
let value = this._unicodeConverter.convertToByteArray(url);
this._cryptoHash.init(this._cryptoHash.MD5);
this._cryptoHash.update(value, value.length);
@@ -502,7 +538,7 @@ this.ReaderMode = {
* @resolves When the cache directory exists.
* @rejects OS.File.Error
*/
- _ensureCacheDir: function () {
+ _ensureCacheDir() {
let dir = OS.Path.join(OS.Constants.Path.profileDir, "readercache");
return OS.File.exists(dir).then(exists => {
if (!exists) {
@@ -510,5 +546,107 @@ this.ReaderMode = {
}
return undefined;
});
- }
+ },
+
+ /**
+ * Sets a global language string value if possible. If langauge detection is
+ * available, use that. Otherwise, revert to a simpler mechanism using the
+ * document's lang attribute or charset.
+ *
+ * @return Promise
+ * @resolves when the language is detected
+ */
+ _assignLanguage(article, attributes) {
+ try {
+ Cu.import("resource://modules/translation/LanguageDetector.jsm");
+ return LanguageDetector.detectLanguage(article.textContent).then(result => {
+ article.language = result.confident ? result.language : null;
+ });
+ } catch(ex) {
+ return new Promise((resolve) => {
+ resolve(this._assignSimpleLanguage(attributes));
+ }).then(result => {
+ article.language = result;
+ });
+ }
+ },
+
+ _assignSimpleLanguage(attributes) {
+ var lang = attributes.lang.substring(0,2);
+ if (lang) {
+ return lang;
+ }
+
+ // If there is no lang attribute, try the charset.
+ // We can only use this for charsets that are specific to one language.
+ const charsetLang = new Map([
+ [ "us-ascii", "en" ],
+ [ "iso-8859-6", "ar" ],
+ [ "iso-8859-7", "el" ],
+ [ "iso-8859-8", "he" ],
+ [ "iso-8859-9", "tr" ],
+ [ "iso-8859-11", "th" ],
+ [ "jis_x0201", "ja" ],
+ [ "shift_jis", "ja" ],
+ [ "euc-jp", "ja" ]
+ ]);
+
+ return charsetLang.get(attributes.charset);
+ },
+
+ _maybeAssignTextDirection(article) {
+ // TODO: Remove the hardcoded language codes below once bug 1320265 is resolved.
+ if (!article.dir && ["ar", "fa", "he", "ug", "ur"].includes(article.language)) {
+ article.dir = "rtl";
+ }
+ },
+
+ /**
+ * Assigns the estimated reading time range of the article to the article object.
+ *
+ * @param article the article object to assign the reading time estimate to.
+ */
+ _assignReadTime(article) {
+ let lang = article.language || "en";
+ const readingSpeed = this._getReadingSpeedForLanguage(lang);
+ const charactersPerMinuteLow = readingSpeed.cpm - readingSpeed.variance;
+ const charactersPerMinuteHigh = readingSpeed.cpm + readingSpeed.variance;
+ const length = article.length;
+
+ article.readingTimeMinsSlow = Math.ceil(length / charactersPerMinuteLow);
+ article.readingTimeMinsFast = Math.ceil(length / charactersPerMinuteHigh);
+ },
+
+ /**
+ * Returns the reading speed of a selection of languages with likely variance.
+ *
+ * Reading speed estimated from a study done on reading speeds in various languages.
+ * study can be found here: http://iovs.arvojournals.org/article.aspx?articleid=2166061
+ *
+ * @return object with characters per minute and variance. Defaults to English
+ * if no suitable language is found in the collection.
+ */
+ _getReadingSpeedForLanguage(lang) {
+ const readingSpeed = new Map([
+ [ "en", {cpm: 987, variance: 118 } ],
+ [ "ar", {cpm: 612, variance: 88 } ],
+ [ "de", {cpm: 920, variance: 86 } ],
+ [ "es", {cpm: 1025, variance: 127 } ],
+ [ "fi", {cpm: 1078, variance: 121 } ],
+ [ "fr", {cpm: 998, variance: 126 } ],
+ [ "he", {cpm: 833, variance: 130 } ],
+ [ "it", {cpm: 950, variance: 140 } ],
+ [ "jw", {cpm: 357, variance: 56 } ],
+ [ "nl", {cpm: 978, variance: 143 } ],
+ [ "pl", {cpm: 916, variance: 126 } ],
+ [ "pt", {cpm: 913, variance: 145 } ],
+ [ "ru", {cpm: 986, variance: 175 } ],
+ [ "sk", {cpm: 885, variance: 145 } ],
+ [ "sv", {cpm: 917, variance: 156 } ],
+ [ "tr", {cpm: 1054, variance: 156 } ],
+ [ "zh", {cpm: 255, variance: 29 } ],
+ ]);
+
+ return readingSpeed.get(lang) || readingSpeed.get("en");
+ },
};
diff --git a/toolkit/components/reader/ReaderWorker.js b/toolkit/components/reader/ReaderWorker.js
index 20023d4e0..9ae589d7d 100644
--- a/toolkit/components/reader/ReaderWorker.js
+++ b/toolkit/components/reader/ReaderWorker.js
@@ -2,6 +2,8 @@
* 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/. */
+/* eslint-env mozilla/chrome-worker */
+
"use strict";
/**
@@ -40,11 +42,12 @@ var Agent = {
*
* @param {object} uri URI data for the document.
* @param {string} serializedDoc The serialized document.
+ * @param {object} options Options object to pass to Readability.
*
* @return {object} Article object returned from Readability.
*/
- parseDocument: function (uri, serializedDoc) {
+ parseDocument(uri, serializedDoc, options) {
let doc = new JSDOMParser().parse(serializedDoc);
- return new Readability(uri, doc).parse();
+ return new Readability(uri, doc, options).parse();
},
};
diff --git a/toolkit/components/reader/content/aboutReader.html b/toolkit/components/reader/content/aboutReader.html
index b9c1139f6..1aa644474 100644
--- a/toolkit/components/reader/content/aboutReader.html
+++ b/toolkit/components/reader/content/aboutReader.html
@@ -7,63 +7,56 @@
<link rel="stylesheet" href="chrome://global/skin/aboutReader.css" type="text/css"/>
- <script type="text/javascript;version=1.8" src="chrome://global/content/reader/aboutReader.js"></script>
+ <script type="text/javascript" src="chrome://global/content/reader/aboutReader.js"></script>
</head>
<body>
- <div id="container" class="container">
- <div id="reader-header" class="header">
- <style scoped>
- @import url("chrome://global/skin/aboutReaderControls.css");
- </style>
- <a id="reader-domain" class="domain"></a>
+ <div class="container">
+ <div class="header reader-header">
+ <a class="domain reader-domain"></a>
<div class="domain-border"></div>
- <h1 id="reader-title"></h1>
- <div id="reader-credits" class="credits"></div>
+ <h1 class="reader-title"></h1>
+ <div class="credits reader-credits"></div>
+ <div class="meta-data">
+ <div class="reader-estimated-time"></div>
+ </div>
</div>
+ <hr>
+
<div class="content">
- <style scoped>
- @import url("chrome://global/skin/aboutReaderContent.css");
- </style>
- <div id="moz-reader-content"></div>
+ <div class="moz-reader-content"></div>
</div>
<div>
- <style scoped>
- @import url("chrome://global/skin/aboutReaderControls.css");
- </style>
- <div id="reader-message"></div>
+ <div class="reader-message"></div>
</div>
</div>
- <ul id="reader-toolbar" class="toolbar">
- <style scoped>
- @import url("chrome://global/skin/aboutReaderControls.css");
- </style>
- <li><button id="close-button" class="button close-button"/></li>
- <ul id="style-dropdown" class="dropdown">
+ <ul class="toolbar reader-toolbar">
+ <li><button class="button close-button"/></li>
+ <ul class="dropdown style-dropdown">
<li><button class="dropdown-toggle button style-button"/></li>
- <li id="reader-popup" class="dropdown-popup">
- <div id="font-type-buttons"></div>
- <hr></hr>
- <div id="font-size-buttons">
- <button id="font-size-minus" class="minus-button"/>
- <button id="font-size-sample"/>
- <button id="font-size-plus" class="plus-button"/>
+ <li class="dropdown-popup">
+ <div class="font-type-buttons"></div>
+ <hr>
+ <div class="font-size-buttons">
+ <button class="minus-button"/>
+ <button class="font-size-sample"/>
+ <button class="plus-button"/>
</div>
- <hr></hr>
- <div id="content-width-buttons">
- <button id="content-width-minus" class="content-width-minus-button"/>
- <button id="content-width-plus" class="content-width-plus-button"/>
+ <hr>
+ <div class="content-width-buttons">
+ <button class="content-width-minus-button"/>
+ <button class="content-width-plus-button"/>
</div>
- <hr></hr>
- <div id="line-height-buttons">
- <button id="line-height-minus" class="line-height-minus-button"/>
- <button id="line-height-plus" class="line-height-plus-button"/>
+ <hr>
+ <div class="line-height-buttons">
+ <button class="line-height-minus-button"/>
+ <button class="line-height-plus-button"/>
</div>
- <hr></hr>
- <div id="color-scheme-buttons"></div>
+ <hr>
+ <div class="color-scheme-buttons"></div>
<div class="dropdown-arrow"/>
</li>
</ul>
diff --git a/toolkit/components/reader/content/aboutReader.js b/toolkit/components/reader/content/aboutReader.js
index 17133e69d..6c963382e 100644
--- a/toolkit/components/reader/content/aboutReader.js
+++ b/toolkit/components/reader/content/aboutReader.js
@@ -4,6 +4,6 @@
"use strict";
-window.addEventListener("DOMContentLoaded", function () {
+window.addEventListener("DOMContentLoaded", function() {
document.dispatchEvent(new CustomEvent("AboutReaderContentLoaded", { bubbles: true }));
});
diff --git a/toolkit/components/startup/nsAppStartup.cpp b/toolkit/components/startup/nsAppStartup.cpp
index 85d5afdf9..fde00d1db 100644
--- a/toolkit/components/startup/nsAppStartup.cpp
+++ b/toolkit/components/startup/nsAppStartup.cpp
@@ -237,7 +237,7 @@ NS_IMPL_ISUPPORTS(nsAppStartup,
NS_IMETHODIMP
nsAppStartup::CreateHiddenWindow()
{
-#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_UIKIT)
+#if defined(MOZ_WIDGET_UIKIT)
return NS_OK;
#else
nsCOMPtr<nsIAppShellService> appShellService
@@ -252,7 +252,7 @@ nsAppStartup::CreateHiddenWindow()
NS_IMETHODIMP
nsAppStartup::DestroyHiddenWindow()
{
-#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_UIKIT)
+#if defined(MOZ_WIDGET_UIKIT)
return NS_OK;
#else
nsCOMPtr<nsIAppShellService> appShellService
diff --git a/toolkit/components/telemetry/Telemetry.cpp b/toolkit/components/telemetry/Telemetry.cpp
index ad2263c9b..6dbd59bcf 100644
--- a/toolkit/components/telemetry/Telemetry.cpp
+++ b/toolkit/components/telemetry/Telemetry.cpp
@@ -1127,7 +1127,7 @@ TelemetryImpl::SnapshotSubsessionHistograms(bool clearSubsession,
JSContext *cx,
JS::MutableHandle<JS::Value> ret)
{
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
return TelemetryHistogram::CreateHistogramSnapshots(cx, ret, true,
clearSubsession);
#else
diff --git a/toolkit/components/telemetry/TelemetryEnvironment.jsm b/toolkit/components/telemetry/TelemetryEnvironment.jsm
index 910d804ae..295679ca4 100644
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -28,11 +28,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "AttributionCode",
"resource:///modules/AttributionCode.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
"resource://gre/modules/ctypes.jsm");
-if (AppConstants.platform !== "gonk") {
- Cu.import("resource://gre/modules/AddonManager.jsm");
- XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
- "resource://gre/modules/LightweightThemeManager.jsm");
-}
+Cu.import("resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
+ "resource://gre/modules/LightweightThemeManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
"resource://gre/modules/ProfileAge.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
@@ -528,11 +526,9 @@ EnvironmentAddonBuilder.prototype = {
_updateAddons: Task.async(function* () {
this._environment._log.trace("_updateAddons");
let personaId = null;
- if (AppConstants.platform !== "gonk") {
- let theme = LightweightThemeManager.currentTheme;
- if (theme) {
- personaId = theme.id;
- }
+ let theme = LightweightThemeManager.currentTheme;
+ if (theme) {
+ personaId = theme.id;
}
let addons = {
@@ -745,14 +741,8 @@ function EnvironmentCache() {
// until the initial environment has been built.
let p = [];
- if (AppConstants.platform === "gonk") {
- this._addonBuilder = {
- watchForChanges: function() {}
- };
- } else {
- this._addonBuilder = new EnvironmentAddonBuilder(this);
- p = [ this._addonBuilder.init() ];
- }
+ this._addonBuilder = new EnvironmentAddonBuilder(this);
+ p = [ this._addonBuilder.init() ];
this._currentEnvironment.profile = {};
p.push(this._updateProfile());
@@ -1079,10 +1069,6 @@ EnvironmentCache.prototype = {
* @returns null on error, true if we are the default browser, or false otherwise.
*/
_isDefaultBrowser: function () {
- if (AppConstants.platform === "gonk") {
- return true;
- }
-
if (!("@mozilla.org/browser/shell-service;1" in Cc)) {
this._log.info("_isDefaultBrowser - Could not obtain browser shell service");
return null;
@@ -1139,10 +1125,8 @@ EnvironmentCache.prototype = {
userPrefs: this._getPrefData(),
};
- if (AppConstants.platform !== "gonk") {
- this._currentEnvironment.settings.addonCompatibilityCheckEnabled =
- AddonManager.checkCompatibility;
- }
+ this._currentEnvironment.settings.addonCompatibilityCheckEnabled =
+ AddonManager.checkCompatibility;
if (AppConstants.platform !== "android") {
this._currentEnvironment.settings.isDefaultBrowser =
@@ -1246,7 +1230,7 @@ EnvironmentCache.prototype = {
* not a portable device.
*/
_getDeviceData: function () {
- if (!["gonk", "android"].includes(AppConstants.platform)) {
+ if (!["android"].includes(AppConstants.platform)) {
return null;
}
@@ -1269,7 +1253,7 @@ EnvironmentCache.prototype = {
locale: forceToStringOrNull(getSystemLocale()),
};
- if (["gonk", "android"].includes(AppConstants.platform)) {
+ if (["android"].includes(AppConstants.platform)) {
data.kernelVersion = forceToStringOrNull(getSysinfoProperty("kernel_version", null));
} else if (AppConstants.platform === "win") {
// The path to the "UBR" key, queried to get additional version details on Windows.
@@ -1333,7 +1317,7 @@ EnvironmentCache.prototype = {
features: {},
};
- if (!["gonk", "android", "linux"].includes(AppConstants.platform)) {
+ if (!["android", "linux"].includes(AppConstants.platform)) {
let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
try {
gfxData.monitors = gfxInfo.getMonitors();
@@ -1398,7 +1382,7 @@ EnvironmentCache.prototype = {
if (AppConstants.platform === "win") {
data.isWow64 = getSysinfoProperty("isWow64", null);
- } else if (["gonk", "android"].includes(AppConstants.platform)) {
+ } else if (["android"].includes(AppConstants.platform)) {
data.device = this._getDeviceData();
}
diff --git a/toolkit/components/telemetry/TelemetryHistogram.cpp b/toolkit/components/telemetry/TelemetryHistogram.cpp
index abae9c613..ba0288979 100644
--- a/toolkit/components/telemetry/TelemetryHistogram.cpp
+++ b/toolkit/components/telemetry/TelemetryHistogram.cpp
@@ -583,7 +583,7 @@ internal_GetHistogramByName(const nsACString &name, Histogram **ret)
}
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
/**
* This clones a histogram |existing| with the id |existingId| to a
@@ -684,7 +684,7 @@ internal_HistogramAdd(Histogram& histogram, int32_t value, uint32_t dataset)
return NS_OK;
}
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
if (Histogram* subsession = internal_GetSubsessionHistogram(histogram)) {
subsession->Add(value);
}
@@ -729,7 +729,7 @@ internal_HistogramClear(Histogram& aHistogram, bool onlySubsession)
aHistogram.Clear();
}
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
if (Histogram* subsession = internal_GetSubsessionHistogram(aHistogram)) {
subsession->Clear();
}
@@ -926,7 +926,7 @@ private:
typedef nsBaseHashtableET<nsCStringHashKey, Histogram*> KeyedHistogramEntry;
typedef AutoHashtable<KeyedHistogramEntry> KeyedHistogramMapType;
KeyedHistogramMapType mHistogramMap;
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
KeyedHistogramMapType mSubsessionMap;
#endif
@@ -950,7 +950,7 @@ KeyedHistogram::KeyedHistogram(const nsACString &name,
uint32_t min, uint32_t max,
uint32_t bucketCount, uint32_t dataset)
: mHistogramMap()
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
, mSubsessionMap()
#endif
, mName(name)
@@ -968,7 +968,7 @@ nsresult
KeyedHistogram::GetHistogram(const nsCString& key, Histogram** histogram,
bool subsession)
{
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
KeyedHistogramMapType& map = subsession ? mSubsessionMap : mHistogramMap;
#else
KeyedHistogramMapType& map = mHistogramMap;
@@ -980,7 +980,7 @@ KeyedHistogram::GetHistogram(const nsCString& key, Histogram** histogram,
}
nsCString histogramName;
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
if (subsession) {
histogramName.AppendLiteral(SUBSESSION_HISTOGRAM_PREFIX);
}
@@ -1042,7 +1042,7 @@ KeyedHistogram::Add(const nsCString& key, uint32_t sample)
if (!histogram) {
return NS_ERROR_FAILURE;
}
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
Histogram* subsession = GetHistogram(key, true);
MOZ_ASSERT(subsession);
if (!subsession) {
@@ -1055,7 +1055,7 @@ KeyedHistogram::Add(const nsCString& key, uint32_t sample)
}
histogram->Add(sample);
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
subsession->Add(sample);
#endif
return NS_OK;
@@ -1068,7 +1068,7 @@ KeyedHistogram::Clear(bool onlySubsession)
if (!XRE_IsParentProcess()) {
return;
}
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
for (auto iter = mSubsessionMap.Iter(); !iter.Done(); iter.Next()) {
iter.Get()->mData->Clear();
}
@@ -1137,7 +1137,7 @@ nsresult
KeyedHistogram::GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
bool subsession, bool clearSubsession)
{
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
KeyedHistogramMapType& map = subsession ? mSubsessionMap : mHistogramMap;
#else
KeyedHistogramMapType& map = mHistogramMap;
@@ -1146,7 +1146,7 @@ KeyedHistogram::GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
return NS_ERROR_FAILURE;
}
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
if (subsession && clearSubsession) {
Clear(true);
}
@@ -1637,7 +1637,7 @@ internal_JSHistogram_Clear(JSContext *cx, unsigned argc, JS::Value *vp)
}
bool onlySubsession = false;
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
if (args.length() >= 1) {
@@ -1874,7 +1874,7 @@ internal_JSKeyedHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
return internal_KeyedHistogram_SnapshotImpl(cx, argc, vp, false, false);
}
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
bool
internal_JSKeyedHistogram_SubsessionSnapshot(JSContext *cx,
unsigned argc, JS::Value *vp)
@@ -1883,7 +1883,7 @@ internal_JSKeyedHistogram_SubsessionSnapshot(JSContext *cx,
}
#endif
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
bool
internal_JSKeyedHistogram_SnapshotSubsessionAndClear(JSContext *cx,
unsigned argc,
@@ -1911,7 +1911,7 @@ internal_JSKeyedHistogram_Clear(JSContext *cx, unsigned argc, JS::Value *vp)
return false;
}
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
bool onlySubsession = false;
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
@@ -1974,7 +1974,7 @@ internal_WrapAndReturnKeyedHistogram(KeyedHistogram *h, JSContext *cx,
if (!(JS_DefineFunction(cx, obj, "add", internal_JSKeyedHistogram_Add, 2, 0)
&& JS_DefineFunction(cx, obj, "snapshot",
internal_JSKeyedHistogram_Snapshot, 1, 0)
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
&& JS_DefineFunction(cx, obj, "subsessionSnapshot",
internal_JSKeyedHistogram_SubsessionSnapshot, 1, 0)
&& JS_DefineFunction(cx, obj, "snapshotSubsessionAndClear",
@@ -2425,7 +2425,7 @@ TelemetryHistogram::CreateHistogramSnapshots(JSContext *cx,
}
Histogram* original = h;
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
if (subsession) {
h = internal_GetSubsessionHistogram(*h);
if (!h) {
@@ -2453,7 +2453,7 @@ TelemetryHistogram::CreateHistogramSnapshots(JSContext *cx,
}
}
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#if !defined(MOZ_WIDGET_ANDROID)
if (subsession && clearSubsession) {
h->Clear();
}
diff --git a/toolkit/components/telemetry/TelemetrySession.jsm b/toolkit/components/telemetry/TelemetrySession.jsm
index 3d97dc155..179e6436a 100644
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -1372,7 +1372,7 @@ var Impl = {
let payload;
try {
- const isMobile = ["gonk", "android"].includes(AppConstants.platform);
+ const isMobile = ["android"].includes(AppConstants.platform);
const isSubsession = isMobile ? false : !this._isClassicReason(reason);
if (isMobile) {
diff --git a/toolkit/components/telemetry/TelemetryStorage.jsm b/toolkit/components/telemetry/TelemetryStorage.jsm
index 91cfc993d..c844aacf0 100644
--- a/toolkit/components/telemetry/TelemetryStorage.jsm
+++ b/toolkit/components/telemetry/TelemetryStorage.jsm
@@ -106,7 +106,7 @@ PingParseError.prototype.constructor = PingParseError;
var Policy = {
now: () => new Date(),
getArchiveQuota: () => ARCHIVE_QUOTA_BYTES,
- getPendingPingsQuota: () => (AppConstants.platform in ["android", "gonk"])
+ getPendingPingsQuota: () => (AppConstants.platform in ["android"])
? PENDING_PINGS_QUOTA_BYTES_MOBILE
: PENDING_PINGS_QUOTA_BYTES_DESKTOP,
};
diff --git a/toolkit/components/telemetry/tests/unit/head.js b/toolkit/components/telemetry/tests/unit/head.js
index 51be25766..87afd3617 100644
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -20,7 +20,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS",
const gIsWindows = AppConstants.platform == "win";
const gIsMac = AppConstants.platform == "macosx";
const gIsAndroid = AppConstants.platform == "android";
-const gIsGonk = AppConstants.platform == "gonk";
+const gIsGonk = false;
const gIsLinux = AppConstants.platform == "linux";
const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);