new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/downloads/DownloadLastDir.jsm
@@ -0,0 +1,131 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Download Manager Utility Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Ehsan Akhgari <ehsan.akhgari@gmail.com>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * The behavior implemented by gDownloadLastDir is documented here.
+ *
+ * In normal browsing sessions, gDownloadLastDir uses the browser.download.lastDir
+ * preference to store the last used download directory. The first time the user
+ * switches into the private browsing mode, the last download directory is
+ * preserved to the pref value, but if the user switches to another directory
+ * during the private browsing mode, that directory is not stored in the pref,
+ * and will be merely kept in memory. When leaving the private browsing mode,
+ * this in-memory value will be discarded, and the last download directory
+ * will be reverted to the pref value.
+ *
+ * Both the pref and the in-memory value will be cleared when clearing the
+ * browsing history. This effectively changes the last download directory
+ * to the default download directory on each platform.
+ */
+
+const LAST_DIR_PREF = "browser.download.lastDir";
+const PBSVC_CID = "@mozilla.org/privatebrowsing;1";
+const nsILocalFile = Components.interfaces.nsILocalFile;
+
+var EXPORTED_SYMBOLS = [ "gDownloadLastDir" ];
+
+let pbSvc = null;
+if (PBSVC_CID in Components.classes) {
+ pbSvc = Components.classes[PBSVC_CID]
+ .getService(Components.interfaces.nsIPrivateBrowsingService);
+}
+let prefSvc = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+
+let observer = {
+ QueryInterface: function (aIID) {
+ if (aIID.equals(Components.interfaces.nsIObserver) ||
+ aIID.equals(Components.interfaces.nsISupports) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "private-browsing":
+ if (aData == "enter")
+ gDownloadLastDirFile = readLastDirPref();
+ else if (aData == "exit")
+ gDownloadLastDirFile = null;
+ break;
+ case "browser:purge-session-history":
+ gDownloadLastDirFile = null;
+ if (prefSvc.prefHasUserValue(LAST_DIR_PREF))
+ prefSvc.clearUserPref(LAST_DIR_PREF);
+ break;
+ }
+ }
+};
+
+let os = Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService);
+os.addObserver(observer, "private-browsing", true);
+os.addObserver(observer, "browser:purge-session-history", true);
+
+function readLastDirPref() {
+ try {
+ return prefSvc.getComplexValue(LAST_DIR_PREF, nsILocalFile);
+ }
+ catch (e) {
+ return null;
+ }
+}
+
+let gDownloadLastDirFile = readLastDirPref();
+let gDownloadLastDir = {
+ get file() {
+ if (gDownloadLastDirFile && !gDownloadLastDirFile.exists())
+ gDownloadLastDirFile = null;
+
+ if (pbSvc && pbSvc.privateBrowsingEnabled)
+ return gDownloadLastDirFile;
+ else
+ return readLastDirPref();
+ },
+ set file(val) {
+ if (pbSvc && pbSvc.privateBrowsingEnabled) {
+ if (val instanceof Components.interfaces.nsIFile)
+ gDownloadLastDirFile = val.clone();
+ else
+ gDownloadLastDirFile = null;
+ } else {
+ if (val instanceof Components.interfaces.nsIFile)
+ prefSvc.setComplexValue(LAST_DIR_PREF, nsILocalFile, val);
+ else if (prefSvc.prefHasUserValue(LAST_DIR_PREF))
+ prefSvc.clearUserPref(LAST_DIR_PREF);
+ }
+ }
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/downloads/DownloadUtils.jsm
@@ -0,0 +1,508 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Download Manager Utility Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Edward Lee <edward.lee@engineering.uiuc.edu>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var EXPORTED_SYMBOLS = [ "DownloadUtils" ];
+
+/**
+ * This module provides the DownloadUtils object which contains useful methods
+ * for downloads such as displaying file sizes, transfer times, and download
+ * locations.
+ *
+ * List of methods:
+ *
+ * [string status, double newLast]
+ * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes,
+ * [optional] double aSpeed, [optional] double aLastSec)
+ *
+ * string progress
+ * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes)
+ *
+ * [string timeLeft, double newLast]
+ * getTimeLeft(double aSeconds, [optional] double aLastSec)
+ *
+ * [string displayHost, string fullHost]
+ * getURIHost(string aURIString)
+ *
+ * [double convertedBytes, string units]
+ * convertByteUnits(int aBytes)
+ *
+ * [int time, string units, int subTime, string subUnits]
+ * convertTimeUnits(double aSecs)
+ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+__defineGetter__("PluralForm", function() {
+ delete this.PluralForm;
+ Cu.import("resource://gre/modules/PluralForm.jsm");
+ return PluralForm;
+});
+
+const kDownloadProperties =
+ "chrome://mozapps/locale/downloads/downloads.properties";
+
+// These strings will be converted to the corresponding ones from the string
+// bundle on use
+let kStrings = {
+ statusFormat: "statusFormat2",
+ transferSameUnits: "transferSameUnits",
+ transferDiffUnits: "transferDiffUnits",
+ transferNoTotal: "transferNoTotal",
+ timePair: "timePair",
+ timeLeftSingle: "timeLeftSingle",
+ timeLeftDouble: "timeLeftDouble",
+ timeFewSeconds: "timeFewSeconds",
+ timeUnknown: "timeUnknown",
+ doneScheme: "doneScheme",
+ doneFileScheme: "doneFileScheme",
+ units: ["bytes", "kilobyte", "megabyte", "gigabyte"],
+ // Update timeSize in convertTimeUnits if changing the length of this array
+ timeUnits: ["seconds", "minutes", "hours", "days"],
+};
+
+// This object will lazily load the strings defined in kStrings
+let gStr = {
+ /**
+ * Initialize lazy string getters
+ */
+ _init: function()
+ {
+ // Make each "name" a lazy-loading string that knows how to load itself. We
+ // need to locally scope name and value to keep them around for the getter.
+ for (let [name, value] in Iterator(kStrings))
+ let ([n, v] = [name, value])
+ gStr.__defineGetter__(n, function() gStr._getStr(n, v));
+ },
+
+ /**
+ * Convert strings to those in the string bundle. This lazily loads the
+ * string bundle *once* only when used the first time.
+ */
+ get _getStr()
+ {
+ // Delete the getter to be overwritten
+ delete gStr._getStr;
+
+ // Lazily load the bundle into the closure on first call to _getStr
+ let getStr = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle(kDownloadProperties).
+ GetStringFromName;
+
+ // _getStr is a function that sets string "name" to stringbundle's "value"
+ return gStr._getStr = function(name, value) {
+ // Delete the getter to be overwritten
+ delete gStr[name];
+
+ try {
+ // "name" is a string or array of the stringbundle-loaded "value"
+ return gStr[name] = typeof value == "string" ?
+ getStr(value) :
+ value.map(getStr);
+ } catch (e) {
+ log(["Couldn't get string '", name, "' from property '", value, "'"]);
+ // Don't return anything (undefined), and because we deleted ourselves,
+ // future accesses will also be undefined
+ }
+ };
+ },
+};
+// Initialize the lazy string getters!
+gStr._init();
+
+// Keep track of at most this many second/lastSec pairs so that multiple calls
+// to getTimeLeft produce the same time left
+const kCachedLastMaxSize = 10;
+let gCachedLast = [];
+
+let DownloadUtils = {
+ /**
+ * Generate a full status string for a download given its current progress,
+ * total size, speed, last time remaining
+ *
+ * @param aCurrBytes
+ * Number of bytes transferred so far
+ * @param [optional] aMaxBytes
+ * Total number of bytes or -1 for unknown
+ * @param [optional] aSpeed
+ * Current transfer rate in bytes/sec or -1 for unknown
+ * @param [optional] aLastSec
+ * Last time remaining in seconds or Infinity for unknown
+ * @return A pair: [download status text, new value of "last seconds"]
+ */
+ getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes,
+ aSpeed, aLastSec)
+ {
+ if (aMaxBytes == null)
+ aMaxBytes = -1;
+ if (aSpeed == null)
+ aSpeed = -1;
+ if (aLastSec == null)
+ aLastSec = Infinity;
+
+ // Calculate the time remaining if we have valid values
+ let seconds = (aSpeed > 0) && (aMaxBytes > 0) ?
+ (aMaxBytes - aCurrBytes) / aSpeed : -1;
+
+ // Update the bytes transferred and bytes total
+ let status;
+ let (transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes)) {
+ // Insert 1 is the download progress
+ status = replaceInsert(gStr.statusFormat, 1, transfer);
+ }
+
+ // Update the download rate
+ let ([rate, unit] = DownloadUtils.convertByteUnits(aSpeed)) {
+ // Insert 2 is the download rate
+ status = replaceInsert(status, 2, rate);
+ // Insert 3 is the |unit|/sec
+ status = replaceInsert(status, 3, unit);
+ }
+
+ // Update time remaining
+ let ([timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec)) {
+ // Insert 4 is the time remaining
+ status = replaceInsert(status, 4, timeLeft);
+
+ return [status, newLast];
+ }
+ },
+
+ /**
+ * Generate the transfer progress string to show the current and total byte
+ * size. Byte units will be as large as possible and the same units for
+ * current and max will be supressed for the former.
+ *
+ * @param aCurrBytes
+ * Number of bytes transferred so far
+ * @param [optional] aMaxBytes
+ * Total number of bytes or -1 for unknown
+ * @return The transfer progress text
+ */
+ getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes)
+ {
+ if (aMaxBytes == null)
+ aMaxBytes = -1;
+
+ let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes);
+ let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes);
+
+ // Figure out which byte progress string to display
+ let transfer;
+ if (total < 0)
+ transfer = gStr.transferNoTotal;
+ else if (progressUnits == totalUnits)
+ transfer = gStr.transferSameUnits;
+ else
+ transfer = gStr.transferDiffUnits;
+
+ transfer = replaceInsert(transfer, 1, progress);
+ transfer = replaceInsert(transfer, 2, progressUnits);
+ transfer = replaceInsert(transfer, 3, total);
+ transfer = replaceInsert(transfer, 4, totalUnits);
+
+ return transfer;
+ },
+
+ /**
+ * Generate a "time left" string given an estimate on the time left and the
+ * last time. The extra time is used to give a better estimate on the time to
+ * show. Both the time values are doubles instead of integers to help get
+ * sub-second accuracy for current and future estimates.
+ *
+ * @param aSeconds
+ * Current estimate on number of seconds left for the download
+ * @param [optional] aLastSec
+ * Last time remaining in seconds or Infinity for unknown
+ * @return A pair: [time left text, new value of "last seconds"]
+ */
+ getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec)
+ {
+ if (aLastSec == null)
+ aLastSec = Infinity;
+
+ if (aSeconds < 0)
+ return [gStr.timeUnknown, aLastSec];
+
+ // Try to find a cached lastSec for the given second
+ aLastSec = gCachedLast.reduce(function(aResult, aItem)
+ aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec);
+
+ // Add the current second/lastSec pair unless we have too many
+ gCachedLast.push([aSeconds, aLastSec]);
+ if (gCachedLast.length > kCachedLastMaxSize)
+ gCachedLast.shift();
+
+ // Apply smoothing only if the new time isn't a huge change -- e.g., if the
+ // new time is more than half the previous time; this is useful for
+ // downloads that start/resume slowly
+ if (aSeconds > aLastSec / 2) {
+ // Apply hysteresis to favor downward over upward swings
+ // 30% of down and 10% of up (exponential smoothing)
+ let (diff = aSeconds - aLastSec) {
+ aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff;
+ }
+
+ // If the new time is similar, reuse something close to the last seconds,
+ // but subtract a little to provide forward progress
+ let diff = aSeconds - aLastSec;
+ let diffPct = diff / aLastSec * 100;
+ if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5)
+ aSeconds = aLastSec - (diff < 0 ? .4 : .2);
+ }
+
+ // Decide what text to show for the time
+ let timeLeft;
+ if (aSeconds < 4) {
+ // Be friendly in the last few seconds
+ timeLeft = gStr.timeFewSeconds;
+ } else {
+ // Convert the seconds into its two largest units to display
+ let [time1, unit1, time2, unit2] =
+ DownloadUtils.convertTimeUnits(aSeconds);
+
+ let pair1 = replaceInsert(gStr.timePair, 1, time1);
+ pair1 = replaceInsert(pair1, 2, unit1);
+ let pair2 = replaceInsert(gStr.timePair, 1, time2);
+ pair2 = replaceInsert(pair2, 2, unit2);
+
+ // Only show minutes for under 1 hour unless there's a few minutes left;
+ // or the second pair is 0.
+ if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) {
+ timeLeft = replaceInsert(gStr.timeLeftSingle, 1, pair1);
+ } else {
+ // We've got 2 pairs of times to display
+ timeLeft = replaceInsert(gStr.timeLeftDouble, 1, pair1);
+ timeLeft = replaceInsert(timeLeft, 2, pair2);
+ }
+ }
+
+ return [timeLeft, aSeconds];
+ },
+
+ /**
+ * Get the appropriate display host string for a URI string depending on if
+ * the URI has an eTLD + 1, is an IP address, a local file, or other protocol
+ *
+ * @param aURIString
+ * The URI string to try getting an eTLD + 1, etc.
+ * @return A pair: [display host for the URI string, full host name]
+ */
+ getURIHost: function DU_getURIHost(aURIString)
+ {
+ let ioService = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
+ getService(Ci.nsIEffectiveTLDService);
+ let idnService = Cc["@mozilla.org/network/idn-service;1"].
+ getService(Ci.nsIIDNService);
+
+ // Get a URI that knows about its components
+ let uri = ioService.newURI(aURIString, null, null);
+
+ // Get the inner-most uri for schemes like jar:
+ if (uri instanceof Ci.nsINestedURI)
+ uri = uri.innermostURI;
+
+ let fullHost;
+ try {
+ // Get the full host name; some special URIs fail (data: jar:)
+ fullHost = uri.host;
+ } catch (e) {
+ fullHost = "";
+ }
+
+ let displayHost;
+ try {
+ // This might fail if it's an IP address or doesn't have more than 1 part
+ let baseDomain = eTLDService.getBaseDomain(uri);
+
+ // Convert base domain for display; ignore the isAscii out param
+ displayHost = idnService.convertToDisplayIDN(baseDomain, {});
+ } catch (e) {
+ // Default to the host name
+ displayHost = fullHost;
+ }
+
+ // Check if we need to show something else for the host
+ if (uri.scheme == "file") {
+ // Display special text for file protocol
+ displayHost = gStr.doneFileScheme;
+ fullHost = displayHost;
+ } else if (displayHost.length == 0) {
+ // Got nothing; show the scheme (data: about: moz-icon:)
+ displayHost = replaceInsert(gStr.doneScheme, 1, uri.scheme);
+ fullHost = displayHost;
+ } else if (uri.port != -1) {
+ // Tack on the port if it's not the default port
+ let port = ":" + uri.port;
+ displayHost += port;
+ fullHost += port;
+ }
+
+ return [displayHost, fullHost];
+ },
+
+ /**
+ * Converts a number of bytes to the appropriate unit that results in a
+ * number that needs fewer than 4 digits
+ *
+ * @param aBytes
+ * Number of bytes to convert
+ * @return A pair: [new value with 3 sig. figs., its unit]
+ */
+ convertByteUnits: function DU_convertByteUnits(aBytes)
+ {
+ let unitIndex = 0;
+
+ // Convert to next unit if it needs 4 digits (after rounding), but only if
+ // we know the name of the next unit
+ while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) {
+ aBytes /= 1024;
+ unitIndex++;
+ }
+
+ // Get rid of insignificant bits by truncating to 1 or 0 decimal points
+ // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
+ aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) ? 1 : 0);
+
+ return [aBytes, gStr.units[unitIndex]];
+ },
+
+ /**
+ * Converts a number of seconds to the two largest units. Time values are
+ * whole numbers, and units have the correct plural/singular form.
+ *
+ * @param aSecs
+ * Seconds to convert into the appropriate 2 units
+ * @return 4-item array [first value, its unit, second value, its unit]
+ */
+ convertTimeUnits: function DU_convertTimeUnits(aSecs)
+ {
+ // These are the maximum values for seconds, minutes, hours corresponding
+ // with gStr.timeUnits without the last item
+ let timeSize = [60, 60, 24];
+
+ let time = aSecs;
+ let scale = 1;
+ let unitIndex = 0;
+
+ // Keep converting to the next unit while we have units left and the
+ // current one isn't the largest unit possible
+ while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) {
+ time /= timeSize[unitIndex];
+ scale *= timeSize[unitIndex];
+ unitIndex++;
+ }
+
+ let value = convertTimeUnitsValue(time);
+ let units = convertTimeUnitsUnits(value, unitIndex);
+
+ let extra = aSecs - value * scale;
+ let nextIndex = unitIndex - 1;
+
+ // Convert the extra time to the next largest unit
+ for (let index = 0; index < nextIndex; index++)
+ extra /= timeSize[index];
+
+ let value2 = convertTimeUnitsValue(extra);
+ let units2 = convertTimeUnitsUnits(value2, nextIndex);
+
+ return [value, units, value2, units2];
+ },
+};
+
+/**
+ * Private helper for convertTimeUnits that gets the display value of a time
+ *
+ * @param aTime
+ * Time value for display
+ * @return An integer value for the time rounded down
+ */
+function convertTimeUnitsValue(aTime)
+{
+ return Math.floor(aTime);
+}
+
+/**
+ * Private helper for convertTimeUnits that gets the display units of a time
+ *
+ * @param aTime
+ * Time value for display
+ * @param aIndex
+ * Index into gStr.timeUnits for the appropriate unit
+ * @return The appropriate plural form of the unit for the time
+ */
+function convertTimeUnitsUnits(aTime, aIndex)
+{
+ // Negative index would be an invalid unit, so just give empty
+ if (aIndex < 0)
+ return "";
+
+ return PluralForm.get(aTime, gStr.timeUnits[aIndex]);
+}
+
+/**
+ * Private helper function to replace a placeholder string with a real string
+ *
+ * @param aText
+ * Source text containing placeholder (e.g., #1)
+ * @param aIndex
+ * Index number of placeholder to replace
+ * @param aValue
+ * New string to put in place of placeholder
+ * @return The string with placeholder replaced with the new string
+ */
+function replaceInsert(aText, aIndex, aValue)
+{
+ return aText.replace("#" + aIndex, aValue);
+}
+
+/**
+ * Private helper function to log errors to the error console and command line
+ *
+ * @param aMsg
+ * Error message to log or an array of strings to concat
+ */
+function log(aMsg)
+{
+ let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg);
+ Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).
+ logStringMessage(msg);
+ dump(msg + "\n");
+}
--- a/toolkit/mozapps/downloads/Makefile.in
+++ b/toolkit/mozapps/downloads/Makefile.in
@@ -30,23 +30,33 @@
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
-DEPTH = ../../..
+DEPTH = ../../..
topsrcdir = @top_srcdir@
srcdir = @srcdir@
-VPATH = @srcdir@
+VPATH = @srcdir@
include $(topsrcdir)/config/config.mk
-DIRS = src
+MODULE = helperAppDlg
+
+EXTRA_COMPONENTS = nsHelperAppDlg.js
+GARBAGE += nsHelperAppDlg.js
+
+EXTRA_JS_MODULES = \
+ DownloadLastDir.jsm \
+ DownloadUtils.jsm \
+ $(NULL)
+
ifdef ENABLE_TESTS
DIRS += tests
endif
include $(topsrcdir)/config/rules.mk
-
+nsHelperAppDlg.js: nsHelperAppDlg.js.in
+ $(PYTHON) $(MOZILLA_DIR)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $^ > $@
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/downloads/nsHelperAppDlg.js.in
@@ -0,0 +1,1238 @@
+/*
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is Mozilla.org Code.
+#
+# The Initial Developer of the Original Code is
+# Doron Rosenberg.
+# Portions created by the Initial Developer are Copyright (C) 2001
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Bill Law <law@netscape.com>
+# Scott MacGregor <mscott@netscape.com>
+# Ben Goodger <ben@bengoodger.com> (2.0)
+# Fredrik Holmqvist <thesuckiestemail@yahoo.se>
+# Dan Mosedale <dmose@mozilla.org>
+# Jim Mathies <jmathies@mozilla.com>
+# Ehsan Akhgari <ehsan.akhgari@gmail.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+*/
+
+///////////////////////////////////////////////////////////////////////////////
+//// Helper Functions
+
+/**
+ * Determines if a given directory is able to be used to download to.
+ *
+ * @param aDirectory
+ * The directory to check.
+ * @returns true if we can use the directory, false otherwise.
+ */
+function isUsableDirectory(aDirectory)
+{
+ return aDirectory.exists() && aDirectory.isDirectory() &&
+ aDirectory.isWritable();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// nsUnkownContentTypeDialog
+
+/* This file implements the nsIHelperAppLauncherDialog interface.
+ *
+ * The implementation consists of a JavaScript "class" named nsUnknownContentTypeDialog,
+ * comprised of:
+ * - a JS constructor function
+ * - a prototype providing all the interface methods and implementation stuff
+ *
+ * In addition, this file implements an nsIModule object that registers the
+ * nsUnknownContentTypeDialog component.
+ */
+
+const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
+const nsITimer = Components.interfaces.nsITimer;
+
+Components.utils.import("resource://gre/modules/DownloadLastDir.jsm");
+
+/* ctor
+ */
+function nsUnknownContentTypeDialog() {
+ // Initialize data properties.
+ this.mLauncher = null;
+ this.mContext = null;
+ this.mSourcePath = null;
+ this.chosenApp = null;
+ this.givenDefaultApp = false;
+ this.updateSelf = true;
+ this.mTitle = "";
+}
+
+nsUnknownContentTypeDialog.prototype = {
+ nsIMIMEInfo : Components.interfaces.nsIMIMEInfo,
+
+ QueryInterface: function (iid) {
+ if (!iid.equals(Components.interfaces.nsIHelperAppLauncherDialog) &&
+ !iid.equals(Components.interfaces.nsITimerCallback) &&
+ !iid.equals(Components.interfaces.nsISupports)) {
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+ },
+
+ // ---------- nsIHelperAppLauncherDialog methods ----------
+
+ // show: Open XUL dialog using window watcher. Since the dialog is not
+ // modal, it needs to be a top level window and the way to open
+ // one of those is via that route).
+ show: function(aLauncher, aContext, aReason) {
+ this.mLauncher = aLauncher;
+ this.mContext = aContext;
+
+ const nsITimer = Components.interfaces.nsITimer;
+ this._showTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(nsITimer);
+ this._showTimer.initWithCallback(this, 0, nsITimer.TYPE_ONE_SHOT);
+ },
+
+ // When opening from new tab, if tab closes while dialog is opening,
+ // (which is a race condition on the XUL file being cached and the timer
+ // in nsExternalHelperAppService), the dialog gets a blur and doesn't
+ // activate the OK button. So we wait a bit before doing opening it.
+ reallyShow: function() {
+ try {
+ var ir = this.mContext.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
+ var dwi = ir.getInterface(Components.interfaces.nsIDOMWindowInternal);
+ var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(Components.interfaces.nsIWindowWatcher);
+ this.mDialog = ww.openWindow(dwi,
+ "chrome://mozapps/content/downloads/unknownContentType.xul",
+ null,
+ "chrome,centerscreen,titlebar,dialog=yes,dependent",
+ null);
+ } catch (ex) {
+ // The containing window may have gone away. Break reference
+ // cycles and stop doing the download.
+ const NS_BINDING_ABORTED = 0x804b0002;
+ this.mLauncher.cancel(NS_BINDING_ABORTED);
+ return;
+ }
+
+ // Hook this object to the dialog.
+ this.mDialog.dialog = this;
+
+ // Hook up utility functions.
+ this.getSpecialFolderKey = this.mDialog.getSpecialFolderKey;
+
+ // Watch for error notifications.
+ this.progressListener.helperAppDlg = this;
+ this.mLauncher.setWebProgressListener(this.progressListener);
+ },
+
+ // promptForSaveToFile: Display file picker dialog and return selected file.
+ // This is called by the External Helper App Service
+ // after the ucth dialog calls |saveToDisk| with a null
+ // target filename (no target, therefore user must pick).
+ //
+ // Alternatively, if the user has selected to have all
+ // files download to a specific location, return that
+ // location and don't ask via the dialog.
+ //
+ // Note - this function is called without a dialog, so it cannot access any part
+ // of the dialog XUL as other functions on this object do.
+ promptForSaveToFile: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) {
+ var result = null;
+
+ this.mLauncher = aLauncher;
+
+ let prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ let bundle = Components.classes["@mozilla.org/intl/stringbundle;1"].
+ getService(Components.interfaces.nsIStringBundleService).
+ createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties");
+
+ if (!aForcePrompt) {
+ // Check to see if the user wishes to auto save to the default download
+ // folder without prompting. Note that preference might not be set.
+ let autodownload = false;
+ try {
+ autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR);
+ } catch (e) { }
+
+ if (autodownload) {
+ // Retrieve the user's default download directory
+ let dnldMgr = Components.classes["@mozilla.org/download-manager;1"]
+ .getService(Components.interfaces.nsIDownloadManager);
+ let defaultFolder = dnldMgr.userDownloadsDirectory;
+
+ try {
+ result = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExtension);
+ }
+ catch (ex) {
+ if (ex.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED) {
+ let prompter = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Components.interfaces.nsIPromptService);
+
+ // Display error alert (using text supplied by back-end)
+ prompter.alert(this.dialog,
+ bundle.GetStringFromName("badPermissions.title"),
+ bundle.GetStringFromName("badPermissions"));
+
+ return;
+ }
+ }
+
+ // Check to make sure we have a valid directory, otherwise, prompt
+ if (result)
+ return result;
+ }
+ }
+
+ // Use file picker to show dialog.
+ var nsIFilePicker = Components.interfaces.nsIFilePicker;
+ var picker = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ var windowTitle = bundle.GetStringFromName("saveDialogTitle");
+ var parent = aContext.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowInternal);
+ picker.init(parent, windowTitle, nsIFilePicker.modeSave);
+ picker.defaultString = aDefaultFile;
+
+ if (aSuggestedFileExtension) {
+ // aSuggestedFileExtension includes the period, so strip it
+ picker.defaultExtension = aSuggestedFileExtension.substring(1);
+ }
+ else {
+ try {
+ picker.defaultExtension = this.mLauncher.MIMEInfo.primaryExtension;
+ }
+ catch (ex) { }
+ }
+
+ var wildCardExtension = "*";
+ if (aSuggestedFileExtension) {
+ wildCardExtension += aSuggestedFileExtension;
+ picker.appendFilter(this.mLauncher.MIMEInfo.description, wildCardExtension);
+ }
+
+ picker.appendFilters( nsIFilePicker.filterAll );
+
+ // Default to lastDir if it is valid, otherwise use the user's default
+ // downloads directory. userDownloadsDirectory should always return a
+ // valid directory, so we can safely default to it.
+ var dnldMgr = Components.classes["@mozilla.org/download-manager;1"]
+ .getService(Components.interfaces.nsIDownloadManager);
+ picker.displayDirectory = dnldMgr.userDownloadsDirectory;
+
+ // The last directory preference may not exist, which will throw.
+ try {
+ var lastDir = gDownloadLastDir.file;
+ if (isUsableDirectory(lastDir))
+ picker.displayDirectory = lastDir;
+ }
+ catch (ex) {
+ }
+
+ if (picker.show() == nsIFilePicker.returnCancel) {
+ // null result means user cancelled.
+ return null;
+ }
+
+ // Be sure to save the directory the user chose through the Save As...
+ // dialog as the new browser.download.dir since the old one
+ // didn't exist.
+ result = picker.file;
+
+ if (result) {
+ try {
+ // Remove the file so that it's not there when we ensure non-existence later;
+ // this is safe because for the file to exist, the user would have had to
+ // confirm that he wanted the file overwritten.
+ if (result.exists())
+ result.remove(false);
+ }
+ catch (e) { }
+ var newDir = result.parent.QueryInterface(Components.interfaces.nsILocalFile);
+
+ // Do not store the last save directory as a pref inside the private browsing mode
+ gDownloadLastDir.file = newDir;
+
+ result = this.validateLeafName(newDir, result.leafName, null);
+ }
+ return result;
+ },
+
+ /**
+ * Ensures that a local folder/file combination does not already exist in
+ * the file system (or finds such a combination with a reasonably similar
+ * leaf name), creates the corresponding file, and returns it.
+ *
+ * @param aLocalFile
+ * the folder where the file resides
+ * @param aLeafName
+ * the string name of the file (may be empty if no name is known,
+ * in which case a name will be chosen)
+ * @param aFileExt
+ * the extension of the file, if one is known; this will be ignored
+ * if aLeafName is non-empty
+ * @returns nsILocalFile
+ * the created file
+ */
+ validateLeafName: function (aLocalFile, aLeafName, aFileExt)
+ {
+ if (!(aLocalFile && isUsableDirectory(aLocalFile)))
+ return null;
+
+ // Remove any leading periods, since we don't want to save hidden files
+ // automatically.
+ aLeafName = aLeafName.replace(/^\.+/, "");
+
+ if (aLeafName == "")
+ aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
+ aLocalFile.append(aLeafName);
+
+ this.makeFileUnique(aLocalFile);
+
+#ifdef XP_WIN
+ let ext;
+ try {
+ // We can fail here if there's no primary extension set
+ ext = "." + this.mLauncher.MIMEInfo.primaryExtension;
+ } catch (e) { }
+
+ // Append a file extension if it's an executable that doesn't have one
+ // but make sure we actually have an extension to add
+ let leaf = aLocalFile.leafName;
+ if (aLocalFile.isExecutable() && ext &&
+ leaf.substring(leaf.length - ext.length) != ext) {
+ let f = aLocalFile.clone();
+ aLocalFile.leafName = leaf + ext;
+
+ f.remove(false);
+ this.makeFileUnique(aLocalFile);
+ }
+#endif
+
+ return aLocalFile;
+ },
+
+ /**
+ * Generates and returns a uniquely-named file from aLocalFile. If
+ * aLocalFile does not exist, it will be the file returned; otherwise, a
+ * file whose name is similar to that of aLocalFile will be returned.
+ */
+ makeFileUnique: function (aLocalFile)
+ {
+ try {
+ // Note - this code is identical to that in
+ // toolkit/content/contentAreaUtils.js.
+ // If you are updating this code, update that code too! We can't share code
+ // here since this is called in a js component.
+ var collisionCount = 0;
+ while (aLocalFile.exists()) {
+ collisionCount++;
+ if (collisionCount == 1) {
+ // Append "(2)" before the last dot in (or at the end of) the filename
+ // special case .ext.gz etc files so we don't wind up with .tar(2).gz
+ if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) {
+ aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
+ }
+ else {
+ aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
+ }
+ }
+ else {
+ // replace the last (n) in the filename with (n+1)
+ aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")");
+ }
+ }
+ aLocalFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0600);
+ }
+ catch (e) {
+ dump("*** exception in validateLeafName: " + e + "\n");
+
+ if (e.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED)
+ throw e;
+
+ if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) {
+ aLocalFile.append("unnamed");
+ if (aLocalFile.exists())
+ aLocalFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0600);
+ }
+ }
+ },
+
+ // ---------- implementation methods ----------
+
+ // Web progress listener so we can detect errors while mLauncher is
+ // streaming the data to a temporary file.
+ progressListener: {
+ // Implementation properties.
+ helperAppDlg: null,
+
+ // nsIWebProgressListener methods.
+ // Look for error notifications and display alert to user.
+ onStatusChange: function( aWebProgress, aRequest, aStatus, aMessage ) {
+ if ( aStatus != Components.results.NS_OK ) {
+ // Get prompt service.
+ var prompter = Components.classes[ "@mozilla.org/embedcomp/prompt-service;1" ]
+ .getService( Components.interfaces.nsIPromptService );
+ // Display error alert (using text supplied by back-end).
+ prompter.alert( this.dialog, this.helperAppDlg.mTitle, aMessage );
+
+ // Close the dialog.
+ this.helperAppDlg.onCancel();
+ if ( this.helperAppDlg.mDialog ) {
+ this.helperAppDlg.mDialog.close();
+ }
+ }
+ },
+
+ // Ignore onProgressChange, onProgressChange64, onStateChange, onLocationChange, onSecurityChange, and onRefreshAttempted notifications.
+ onProgressChange: function( aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress ) {
+ },
+
+ onProgressChange64: function( aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress ) {
+ },
+
+
+
+ onStateChange: function( aWebProgress, aRequest, aStateFlags, aStatus ) {
+ },
+
+ onLocationChange: function( aWebProgress, aRequest, aLocation ) {
+ },
+
+ onSecurityChange: function( aWebProgress, aRequest, state ) {
+ },
+
+ onRefreshAttempted: function( aWebProgress, aURI, aDelay, aSameURI ) {
+ return true;
+ }
+ },
+
+ // initDialog: Fill various dialog fields with initial content.
+ initDialog : function() {
+ // Put file name in window title.
+ var suggestedFileName = this.mLauncher.suggestedFileName;
+
+ // Some URIs do not implement nsIURL, so we can't just QI.
+ var url = this.mLauncher.source;
+ var fname = "";
+ this.mSourcePath = url.prePath;
+ try {
+ url = url.QueryInterface( Components.interfaces.nsIURL );
+ // A url, use file name from it.
+ fname = url.fileName;
+ this.mSourcePath += url.directory;
+ } catch (ex) {
+ // A generic uri, use path.
+ fname = url.path;
+ this.mSourcePath += url.path;
+ }
+
+ if (suggestedFileName)
+ fname = suggestedFileName;
+
+ var displayName = fname.replace(/ +/g, " ");
+
+ this.mTitle = this.dialogElement("strings").getFormattedString("title", [displayName]);
+ this.mDialog.document.title = this.mTitle;
+
+ // Put content type, filename and location into intro.
+ this.initIntro(url, fname, displayName);
+
+ var iconString = "moz-icon://" + fname + "?size=16&contentType=" + this.mLauncher.MIMEInfo.MIMEType;
+ this.dialogElement("contentTypeImage").setAttribute("src", iconString);
+
+ // if always-save and is-executable and no-handler
+ // then set up simple ui
+ var mimeType = this.mLauncher.MIMEInfo.MIMEType;
+ var shouldntRememberChoice = (mimeType == "application/octet-stream" ||
+ mimeType == "application/x-msdownload" ||
+ this.mLauncher.targetFileIsExecutable);
+ if (shouldntRememberChoice && !this.openWithDefaultOK()) {
+ // hide featured choice
+ this.dialogElement("normalBox").collapsed = true;
+ // show basic choice
+ this.dialogElement("basicBox").collapsed = false;
+ // change button labels and icons; use "save" icon for the accept
+ // button since it's the only action possible
+ let acceptButton = this.mDialog.document.documentElement
+ .getButton("accept");
+ acceptButton.label = this.dialogElement("strings")
+ .getString("unknownAccept.label");
+ acceptButton.setAttribute("icon", "save");
+ this.mDialog.document.documentElement.getButton("cancel").label = this.dialogElement("strings").getString("unknownCancel.label");
+ // hide other handler
+ this.dialogElement("openHandler").collapsed = true;
+ // set save as the selected option
+ this.dialogElement("mode").selectedItem = this.dialogElement("save");
+ }
+ else {
+ this.initAppAndSaveToDiskValues();
+
+ // Initialize "always ask me" box. This should always be disabled
+ // and set to true for the ambiguous type application/octet-stream.
+ // We don't also check for application/x-msdownload here since we
+ // want users to be able to autodownload .exe files.
+ var rememberChoice = this.dialogElement("rememberChoice");
+
+#if 0
+ // Just because we have a content-type of application/octet-stream
+ // here doesn't actually mean that the content is of that type. Many
+ // servers default to sending text/plain for file types they don't know
+ // about. To account for this, the uriloader does some checking to see
+ // if a file sent as text/plain contains binary characters, and if so (*)
+ // it morphs the content-type into application/octet-stream so that
+ // the file can be properly handled. Since this is not generic binary
+ // data, rather, a data format that the system probably knows about,
+ // we don't want to use the content-type provided by this dialog's
+ // opener, as that's the generic application/octet-stream that the
+ // uriloader has passed, rather we want to ask the MIME Service.
+ // This is so we don't needlessly disable the "autohandle" checkbox.
+ var mimeService = Components.classes["@mozilla.org/mime;1"].getService(Components.interfaces.nsIMIMEService);
+ var type = mimeService.getTypeFromURI(this.mLauncher.source);
+ this.realMIMEInfo = mimeService.getFromTypeAndExtension(type, "");
+
+ if (type == "application/octet-stream") {
+#endif
+ if (shouldntRememberChoice) {
+ rememberChoice.checked = false;
+ rememberChoice.disabled = true;
+ }
+ else {
+ rememberChoice.checked = !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling;
+ }
+ this.toggleRememberChoice(rememberChoice);
+
+ // XXXben - menulist won't init properly, hack.
+ var openHandler = this.dialogElement("openHandler");
+ openHandler.parentNode.removeChild(openHandler);
+ var openHandlerBox = this.dialogElement("openHandlerBox");
+ openHandlerBox.appendChild(openHandler);
+ }
+
+ this.mDialog.setTimeout("dialog.postShowCallback()", 0);
+
+ this.mDialog.document.documentElement.getButton("accept").disabled = true;
+ this._showTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(nsITimer);
+ this._showTimer.initWithCallback(this, 250, nsITimer.TYPE_ONE_SHOT);
+ },
+
+ notify: function (aTimer) {
+ if (aTimer == this._showTimer) {
+ if (!this.mDialog) {
+ this.reallyShow();
+ } else {
+ // The user may have already canceled the dialog.
+ try {
+ if (!this._blurred) {
+ this.mDialog.document.documentElement.getButton("accept").disabled = false;
+ }
+ } catch (ex) {}
+ this._delayExpired = true;
+ }
+ // The timer won't release us, so we have to release it.
+ this._showTimer = null;
+ }
+ else if (aTimer == this._saveToDiskTimer) {
+ // Since saveToDisk may open a file picker and therefore block this routine,
+ // we should only call it once the dialog is closed.
+ this.mLauncher.saveToDisk(null, false);
+ this._saveToDiskTimer = null;
+ }
+ },
+
+ postShowCallback: function () {
+ this.mDialog.sizeToContent();
+
+ // Set initial focus
+ this.dialogElement("mode").focus();
+ },
+
+ // initIntro:
+ initIntro: function(url, filename, displayname) {
+ this.dialogElement( "location" ).value = displayname;
+ this.dialogElement( "location" ).setAttribute("realname", filename);
+ this.dialogElement( "location" ).setAttribute("tooltiptext", displayname);
+
+ // if mSourcePath is a local file, then let's use the pretty path name instead of an ugly
+ // url...
+ var pathString = this.mSourcePath;
+ try
+ {
+ var fileURL = url.QueryInterface(Components.interfaces.nsIFileURL);
+ if (fileURL)
+ {
+ var fileObject = fileURL.file;
+ if (fileObject)
+ {
+ var parentObject = fileObject.parent;
+ if (parentObject)
+ {
+ pathString = parentObject.path;
+ }
+ }
+ }
+ } catch(ex) {}
+
+ if (pathString == this.mSourcePath)
+ {
+ // wasn't a fileURL
+ var tmpurl = url.clone(); // don't want to change the real url
+ try {
+ tmpurl.userPass = "";
+ } catch (ex) {}
+ pathString = tmpurl.prePath;
+ }
+
+ // Set the location text, which is separate from the intro text so it can be cropped
+ var location = this.dialogElement( "source" );
+ location.value = pathString;
+ location.setAttribute("tooltiptext", this.mSourcePath);
+
+ // Show the type of file.
+ var type = this.dialogElement("type");
+ var mimeInfo = this.mLauncher.MIMEInfo;
+
+ // 1. Try to use the pretty description of the type, if one is available.
+ var typeString = mimeInfo.description;
+
+ if (typeString == "") {
+ // 2. If there is none, use the extension to identify the file, e.g. "ZIP file"
+ var primaryExtension = "";
+ try {
+ primaryExtension = mimeInfo.primaryExtension;
+ }
+ catch (ex) {
+ }
+ if (primaryExtension != "")
+ typeString = this.dialogElement("strings").getFormattedString("fileType", [primaryExtension.toUpperCase()]);
+ // 3. If we can't even do that, just give up and show the MIME type.
+ else
+ typeString = mimeInfo.MIMEType;
+ }
+
+ type.value = typeString;
+ },
+
+ _blurred: false,
+ _delayExpired: false,
+ onBlur: function(aEvent) {
+ this._blurred = true;
+ this.mDialog.document.documentElement.getButton("accept").disabled = true;
+ },
+
+ onFocus: function(aEvent) {
+ this._blurred = false;
+ if (this._delayExpired) {
+ var script = "document.documentElement.getButton('accept').disabled = false";
+ this.mDialog.setTimeout(script, 250);
+ }
+ },
+
+ // Returns true if opening the default application makes sense.
+ openWithDefaultOK: function() {
+ // The checking is different on Windows...
+#ifdef XP_WIN
+ // Windows presents some special cases.
+ // We need to prevent use of "system default" when the file is
+ // executable (so the user doesn't launch nasty programs downloaded
+ // from the web), and, enable use of "system default" if it isn't
+ // executable (because we will prompt the user for the default app
+ // in that case).
+
+ // Default is Ok if the file isn't executable (and vice-versa).
+ return !this.mLauncher.targetFileIsExecutable;
+#else
+ // On other platforms, default is Ok if there is a default app.
+ // Note that nsIMIMEInfo providers need to ensure that this holds true
+ // on each platform.
+ return this.mLauncher.MIMEInfo.hasDefaultHandler;
+#endif
+ },
+
+ // Set "default" application description field.
+ initDefaultApp: function() {
+ // Use description, if we can get one.
+ var desc = this.mLauncher.MIMEInfo.defaultDescription;
+ if (desc) {
+ var defaultApp = this.dialogElement("strings").getFormattedString("defaultApp", [desc]);
+ this.dialogElement("defaultHandler").label = defaultApp;
+ }
+ else {
+ this.dialogElement("modeDeck").setAttribute("selectedIndex", "1");
+ // Hide the default handler item too, in case the user picks a
+ // custom handler at a later date which triggers the menulist to show.
+ this.dialogElement("defaultHandler").hidden = true;
+ }
+ },
+
+ // getPath:
+ getPath: function (aFile) {
+#ifdef XP_MACOSX
+ return aFile.leafName || aFile.path;
+#else
+ return aFile.path;
+#endif
+ },
+
+ // initAppAndSaveToDiskValues:
+ initAppAndSaveToDiskValues: function() {
+ var modeGroup = this.dialogElement("mode");
+
+ // We don't let users open .exe files or random binary data directly
+ // from the browser at the moment because of security concerns.
+ var openWithDefaultOK = this.openWithDefaultOK();
+ var mimeType = this.mLauncher.MIMEInfo.MIMEType;
+ if (this.mLauncher.targetFileIsExecutable || (
+ (mimeType == "application/octet-stream" ||
+ mimeType == "application/x-msdownload") &&
+ !openWithDefaultOK)) {
+ this.dialogElement("open").disabled = true;
+ var openHandler = this.dialogElement("openHandler");
+ openHandler.disabled = true;
+ openHandler.selectedItem = null;
+ modeGroup.selectedItem = this.dialogElement("save");
+ return;
+ }
+
+ // Fill in helper app info, if there is any.
+ try {
+ this.chosenApp =
+ this.mLauncher.MIMEInfo.preferredApplicationHandler
+ .QueryInterface(Components.interfaces.nsILocalHandlerApp);
+ } catch (e) {
+ this.chosenApp = null;
+ }
+ // Initialize "default application" field.
+ this.initDefaultApp();
+
+ var otherHandler = this.dialogElement("otherHandler");
+
+ // Fill application name textbox.
+ if (this.chosenApp && this.chosenApp.executable &&
+ this.chosenApp.executable.path) {
+ otherHandler.setAttribute("path",
+ this.getPath(this.chosenApp.executable));
+
+#if XP_MACOSX
+ this.chosenApp.executable.QueryInterface(Components.interfaces.nsILocalFileMac);
+ otherHandler.label = this.chosenApp.executable.bundleDisplayName;
+#else
+ otherHandler.label = this.chosenApp.executable.leafName;
+#endif
+ otherHandler.hidden = false;
+ }
+
+ var useDefault = this.dialogElement("useSystemDefault");
+ var openHandler = this.dialogElement("openHandler");
+ openHandler.selectedIndex = 0;
+
+ if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useSystemDefault) {
+ // Open (using system default).
+ modeGroup.selectedItem = this.dialogElement("open");
+ } else if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useHelperApp) {
+ // Open with given helper app.
+ modeGroup.selectedItem = this.dialogElement("open");
+ openHandler.selectedIndex = 1;
+ } else {
+ // Save to disk.
+ modeGroup.selectedItem = this.dialogElement("save");
+ }
+
+ // If we don't have a "default app" then disable that choice.
+ if (!openWithDefaultOK) {
+ var useDefault = this.dialogElement("defaultHandler");
+ var isSelected = useDefault.selected;
+
+ // Disable that choice.
+ useDefault.hidden = true;
+ // If that's the default, then switch to "save to disk."
+ if (isSelected) {
+ openHandler.selectedIndex = 1;
+ modeGroup.selectedItem = this.dialogElement("save");
+ }
+ }
+
+ otherHandler.nextSibling.hidden = otherHandler.nextSibling.nextSibling.hidden = false;
+ this.updateOKButton();
+ },
+
+ // Returns the user-selected application
+ helperAppChoice: function() {
+ return this.chosenApp;
+ },
+
+ get saveToDisk() {
+ return this.dialogElement("save").selected;
+ },
+
+ get useOtherHandler() {
+ return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 1;
+ },
+
+ get useSystemDefault() {
+ return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 0;
+ },
+
+ toggleRememberChoice: function (aCheckbox) {
+ this.dialogElement("settingsChange").hidden = !aCheckbox.checked;
+ this.mDialog.sizeToContent();
+ },
+
+ openHandlerCommand: function () {
+ var openHandler = this.dialogElement("openHandler");
+ if (openHandler.selectedItem.id == "choose")
+ this.chooseApp();
+ else
+ openHandler.setAttribute("lastSelectedItemID", openHandler.selectedItem.id);
+ },
+
+ updateOKButton: function() {
+ var ok = false;
+ if (this.dialogElement("save").selected) {
+ // This is always OK.
+ ok = true;
+ }
+ else if (this.dialogElement("open").selected) {
+ switch (this.dialogElement("openHandler").selectedIndex) {
+ case 0:
+ // No app need be specified in this case.
+ ok = true;
+ break;
+ case 1:
+ // only enable the OK button if we have a default app to use or if
+ // the user chose an app....
+ ok = this.chosenApp || /\S/.test(this.dialogElement("otherHandler").getAttribute("path"));
+ break;
+ }
+ }
+
+ // Enable Ok button if ok to press.
+ this.mDialog.document.documentElement.getButton("accept").disabled = !ok;
+ },
+
+ // Returns true iff the user-specified helper app has been modified.
+ appChanged: function() {
+ return this.helperAppChoice() != this.mLauncher.MIMEInfo.preferredApplicationHandler;
+ },
+
+ updateMIMEInfo: function() {
+ var needUpdate = false;
+ // If current selection differs from what's in the mime info object,
+ // then we need to update.
+ if (this.saveToDisk) {
+ needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.saveToDisk;
+ if (needUpdate)
+ this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.saveToDisk;
+ }
+ else if (this.useSystemDefault) {
+ needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useSystemDefault;
+ if (needUpdate)
+ this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useSystemDefault;
+ }
+ else {
+ // For "open with", we need to check both preferred action and whether the user chose
+ // a new app.
+ needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useHelperApp || this.appChanged();
+ if (needUpdate) {
+ this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useHelperApp;
+ // App may have changed - Update application
+ var app = this.helperAppChoice();
+ this.mLauncher.MIMEInfo.preferredApplicationHandler = app;
+ }
+ }
+ // We will also need to update if the "always ask" flag has changed.
+ needUpdate = needUpdate || this.mLauncher.MIMEInfo.alwaysAskBeforeHandling != (!this.dialogElement("rememberChoice").checked);
+
+ // One last special case: If the input "always ask" flag was false, then we always
+ // update. In that case we are displaying the helper app dialog for the first
+ // time for this mime type and we need to store the user's action in the mimeTypes.rdf
+ // data source (whether that action has changed or not; if it didn't change, then we need
+ // to store the "always ask" flag so the helper app dialog will or won't display
+ // next time, per the user's selection).
+ needUpdate = needUpdate || !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling;
+
+ // Make sure mime info has updated setting for the "always ask" flag.
+ this.mLauncher.MIMEInfo.alwaysAskBeforeHandling = !this.dialogElement("rememberChoice").checked;
+
+ return needUpdate;
+ },
+
+ // See if the user changed things, and if so, update the
+ // mimeTypes.rdf entry for this mime type.
+ updateHelperAppPref: function() {
+ var ha = new this.mDialog.HelperApps();
+ ha.updateTypeInfo(this.mLauncher.MIMEInfo);
+ ha.destroy();
+ },
+
+ // onOK:
+ onOK: function() {
+ // Verify typed app path, if necessary.
+ if (this.useOtherHandler) {
+ var helperApp = this.helperAppChoice();
+ if (!helperApp || !helperApp.executable ||
+ !helperApp.executable.exists()) {
+ // Show alert and try again.
+ var bundle = this.dialogElement("strings");
+ var msg = bundle.getFormattedString("badApp", [this.dialogElement("otherHandler").getAttribute("path")]);
+ var svc = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService);
+ svc.alert(this.mDialog, bundle.getString("badApp.title"), msg);
+
+ // Disable the OK button.
+ this.mDialog.document.documentElement.getButton("accept").disabled = true;
+ this.dialogElement("mode").focus();
+
+ // Clear chosen application.
+ this.chosenApp = null;
+
+ // Leave dialog up.
+ return false;
+ }
+ }
+
+ // Remove our web progress listener (a progress dialog will be
+ // taking over).
+ this.mLauncher.setWebProgressListener(null);
+
+ // saveToDisk and launchWithApplication can return errors in
+ // certain circumstances (e.g. The user clicks cancel in the
+ // "Save to Disk" dialog. In those cases, we don't want to
+ // update the helper application preferences in the RDF file.
+ try {
+ var needUpdate = this.updateMIMEInfo();
+
+ if (this.dialogElement("save").selected) {
+ // If we're using a default download location, create a path
+ // for the file to be saved to to pass to |saveToDisk| - otherwise
+ // we must ask the user to pick a save name.
+
+#if 0
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+ var targetFile = null;
+ try {
+ targetFile = prefs.getComplexValue("browser.download.defaultFolder",
+ Components.interfaces.nsILocalFile);
+ var leafName = this.dialogElement("location").getAttribute("realname");
+ // Ensure that we don't overwrite any existing files here.
+ targetFile = this.validateLeafName(targetFile, leafName, null);
+ }
+ catch(e) { }
+
+ this.mLauncher.saveToDisk(targetFile, false);
+#endif
+
+ // see @notify
+ // we cannot use opener's setTimeout, see bug 420405
+ this._saveToDiskTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(nsITimer);
+ this._saveToDiskTimer.initWithCallback(this, 0,
+ nsITimer.TYPE_ONE_SHOT);
+ }
+ else
+ this.mLauncher.launchWithApplication(null, false);
+
+ // Update user pref for this mime type (if necessary). We do not
+ // store anything in the mime type preferences for the ambiguous
+ // type application/octet-stream. We do NOT do this for
+ // application/x-msdownload since we want users to be able to
+ // autodownload these to disk.
+ if (needUpdate && this.mLauncher.MIMEInfo.MIMEType != "application/octet-stream")
+ this.updateHelperAppPref();
+ } catch(e) { }
+
+ // Unhook dialog from this object.
+ this.mDialog.dialog = null;
+
+ // Close up dialog by returning true.
+ return true;
+ },
+
+ // onCancel:
+ onCancel: function() {
+ // Remove our web progress listener.
+ this.mLauncher.setWebProgressListener(null);
+
+ // Cancel app launcher.
+ try {
+ const NS_BINDING_ABORTED = 0x804b0002;
+ this.mLauncher.cancel(NS_BINDING_ABORTED);
+ } catch(exception) {
+ }
+
+ // Unhook dialog from this object.
+ this.mDialog.dialog = null;
+
+ // Close up dialog by returning true.
+ return true;
+ },
+
+ // dialogElement: Convenience.
+ dialogElement: function(id) {
+ return this.mDialog.document.getElementById(id);
+ },
+
+ // Retrieve the pretty description from the file
+ getFileDisplayName: function getFileDisplayName(file)
+ {
+#ifdef XP_WIN
+ if (file instanceof Components.interfaces.nsILocalFileWin) {
+ try {
+ return file.getVersionInfoField("FileDescription");
+ } catch (ex) {
+ }
+ }
+#endif
+ return file.leafName;
+ },
+
+ // chooseApp: Open file picker and prompt user for application.
+ chooseApp: function() {
+#ifdef XP_WIN
+ // Protect against the lack of an extension
+ var fileExtension = "";
+ try {
+ fileExtension = this.mLauncher.MIMEInfo.primaryExtension;
+ } catch(ex) {
+ }
+
+ // Try to use the pretty description of the type, if one is available.
+ var typeString = this.mLauncher.MIMEInfo.description;
+
+ if (!typeString) {
+ // If there is none, use the extension to
+ // identify the file, e.g. "ZIP file"
+ if (fileExtension) {
+ typeString =
+ this.dialogElement("strings").
+ getFormattedString("fileType", [fileExtension.toUpperCase()]);
+ } else {
+ // If we can't even do that, just give up and show the MIME type.
+ typeString = this.mLauncher.MIMEInfo.MIMEType;
+ }
+ }
+
+ var params = {};
+ params.title =
+ this.dialogElement("strings").getString("chooseAppFilePickerTitle");
+ params.description = typeString;
+ params.filename = this.mLauncher.suggestedFileName;
+ params.mimeInfo = this.mLauncher.MIMEInfo;
+ params.handlerApp = null;
+
+ this.mDialog.openDialog("chrome://global/content/appPicker.xul", null,
+ "chrome,modal,centerscreen,titlebar,dialog=yes",
+ params);
+
+ if (params.handlerApp &&
+ params.handlerApp.executable &&
+ params.handlerApp.executable.isFile()) {
+ // Show the "handler" menulist since we have a (user-specified)
+ // application now.
+ this.dialogElement("modeDeck").setAttribute("selectedIndex", "0");
+
+ // Remember the file they chose to run.
+ this.chosenApp = params.handlerApp;
+
+ // Update dialog
+ var otherHandler = this.dialogElement("otherHandler");
+ otherHandler.removeAttribute("hidden");
+ otherHandler.setAttribute("path",
+ this.getPath(this.chosenApp.executable));
+ otherHandler.label =
+ this.getFileDisplayName(this.chosenApp.executable);
+ this.dialogElement("openHandler").selectedIndex = 1;
+ this.dialogElement("openHandler").setAttribute("lastSelectedItemID",
+ "otherHandler");
+ this.dialogElement("mode").selectedItem = this.dialogElement("open");
+ } else {
+ var openHandler = this.dialogElement("openHandler");
+ var lastSelectedID = openHandler.getAttribute("lastSelectedItemID");
+ if (!lastSelectedID)
+ lastSelectedID = "defaultHandler";
+ openHandler.selectedItem = this.dialogElement(lastSelectedID);
+ }
+
+#else
+ var nsIFilePicker = Components.interfaces.nsIFilePicker;
+ var fp = Components.classes["@mozilla.org/filepicker;1"]
+ .createInstance(nsIFilePicker);
+ fp.init(this.mDialog,
+ this.dialogElement("strings").getString("chooseAppFilePickerTitle"),
+ nsIFilePicker.modeOpen);
+
+ fp.appendFilters(nsIFilePicker.filterApps);
+
+ if (fp.show() == nsIFilePicker.returnOK && fp.file) {
+ // Show the "handler" menulist since we have a (user-specified)
+ // application now.
+ this.dialogElement("modeDeck").setAttribute("selectedIndex", "0");
+
+ // Remember the file they chose to run.
+ var localHandlerApp =
+ Components.classes["@mozilla.org/uriloader/local-handler-app;1"].
+ createInstance(Components.interfaces.nsILocalHandlerApp);
+ localHandlerApp.executable = fp.file;
+ this.chosenApp = localHandlerApp;
+
+ // Update dialog.
+ var otherHandler = this.dialogElement("otherHandler");
+ otherHandler.removeAttribute("hidden");
+ otherHandler.setAttribute("path", this.getPath(this.chosenApp.executable));
+#ifdef XP_MACOSX
+ this.chosenApp.executable
+ .QueryInterface(Components.interfaces.nsILocalFileMac);
+ otherHandler.label = this.chosenApp.executable.bundleDisplayName;
+#else
+ otherHandler.label = this.chosenApp.executable.leafName;
+#endif
+ this.dialogElement("openHandler").selectedIndex = 1;
+ this.dialogElement("openHandler").setAttribute("lastSelectedItemID", "otherHandler");
+
+ this.dialogElement("mode").selectedItem = this.dialogElement("open");
+ }
+ else {
+ var openHandler = this.dialogElement("openHandler");
+ var lastSelectedID = openHandler.getAttribute("lastSelectedItemID");
+ if (!lastSelectedID)
+ lastSelectedID = "defaultHandler";
+ openHandler.selectedItem = this.dialogElement(lastSelectedID);
+ }
+#endif
+ },
+
+ // Turn this on to get debugging messages.
+ debug: false,
+
+ // Dump text (if debug is on).
+ dump: function( text ) {
+ if ( this.debug ) {
+ dump( text );
+ }
+ },
+
+ // dumpInfo:
+ doDebug: function() {
+ const nsIProgressDialog = Components.interfaces.nsIProgressDialog;
+ // Open new progress dialog.
+ var progress = Components.classes[ "@mozilla.org/progressdialog;1" ]
+ .createInstance( nsIProgressDialog );
+ // Show it.
+ progress.open( this.mDialog );
+ },
+
+ // dumpObj:
+ dumpObj: function( spec ) {
+ var val = "<undefined>";
+ try {
+ val = eval( "this."+spec ).toString();
+ } catch( exception ) {
+ }
+ this.dump( spec + "=" + val + "\n" );
+ },
+
+ // dumpObjectProperties
+ dumpObjectProperties: function( desc, obj ) {
+ for( prop in obj ) {
+ this.dump( desc + "." + prop + "=" );
+ var val = "<undefined>";
+ try {
+ val = obj[ prop ];
+ } catch ( exception ) {
+ }
+ this.dump( val + "\n" );
+ }
+ }
+}
+
+// This Component's module implementation. All the code below is used to get this
+// component registered and accessible via XPCOM.
+var module = {
+ // registerSelf: Register this component.
+ registerSelf: function (compMgr, fileSpec, location, type) {
+ compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
+
+ compMgr.registerFactoryLocation( this.cid,
+ "Unknown Content Type Dialog",
+ this.contractId,
+ fileSpec,
+ location,
+ type );
+ },
+
+ // getClassObject: Return this component's factory object.
+ getClassObject: function (compMgr, cid, iid) {
+ if (!cid.equals(this.cid)) {
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+
+ if (!iid.equals(Components.interfaces.nsIFactory)) {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ return this.factory;
+ },
+
+ /* CID for this class */
+ cid: Components.ID("{F68578EB-6EC2-4169-AE19-8C6243F0ABE1}"),
+
+ /* Contract ID for this class */
+ contractId: "@mozilla.org/helperapplauncherdialog;1",
+
+ /* factory object */
+ factory: {
+ // createInstance: Return a new nsProgressDialog object.
+ createInstance: function (outer, iid) {
+ if (outer != null)
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+
+ return (new nsUnknownContentTypeDialog()).QueryInterface(iid);
+ }
+ },
+
+ // canUnload: n/a (returns true)
+ canUnload: function(compMgr) {
+ return true;
+ }
+};
+
+// NSGetModule: Return the nsIModule object.
+function NSGetModule(compMgr, fileSpec) {
+ return module;
+}
deleted file mode 100644
--- a/toolkit/mozapps/downloads/src/DownloadLastDir.jsm
+++ /dev/null
@@ -1,131 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Download Manager Utility Code.
- *
- * The Initial Developer of the Original Code is
- * Ehsan Akhgari <ehsan.akhgari@gmail.com>.
- * Portions created by the Initial Developer are Copyright (C) 2008
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-/*
- * The behavior implemented by gDownloadLastDir is documented here.
- *
- * In normal browsing sessions, gDownloadLastDir uses the browser.download.lastDir
- * preference to store the last used download directory. The first time the user
- * switches into the private browsing mode, the last download directory is
- * preserved to the pref value, but if the user switches to another directory
- * during the private browsing mode, that directory is not stored in the pref,
- * and will be merely kept in memory. When leaving the private browsing mode,
- * this in-memory value will be discarded, and the last download directory
- * will be reverted to the pref value.
- *
- * Both the pref and the in-memory value will be cleared when clearing the
- * browsing history. This effectively changes the last download directory
- * to the default download directory on each platform.
- */
-
-const LAST_DIR_PREF = "browser.download.lastDir";
-const PBSVC_CID = "@mozilla.org/privatebrowsing;1";
-const nsILocalFile = Components.interfaces.nsILocalFile;
-
-var EXPORTED_SYMBOLS = [ "gDownloadLastDir" ];
-
-let pbSvc = null;
-if (PBSVC_CID in Components.classes) {
- pbSvc = Components.classes[PBSVC_CID]
- .getService(Components.interfaces.nsIPrivateBrowsingService);
-}
-let prefSvc = Components.classes["@mozilla.org/preferences-service;1"]
- .getService(Components.interfaces.nsIPrefBranch);
-
-let observer = {
- QueryInterface: function (aIID) {
- if (aIID.equals(Components.interfaces.nsIObserver) ||
- aIID.equals(Components.interfaces.nsISupports) ||
- aIID.equals(Components.interfaces.nsISupportsWeakReference))
- return this;
- throw Components.results.NS_NOINTERFACE;
- },
- observe: function (aSubject, aTopic, aData) {
- switch (aTopic) {
- case "private-browsing":
- if (aData == "enter")
- gDownloadLastDirFile = readLastDirPref();
- else if (aData == "exit")
- gDownloadLastDirFile = null;
- break;
- case "browser:purge-session-history":
- gDownloadLastDirFile = null;
- if (prefSvc.prefHasUserValue(LAST_DIR_PREF))
- prefSvc.clearUserPref(LAST_DIR_PREF);
- break;
- }
- }
-};
-
-let os = Components.classes["@mozilla.org/observer-service;1"]
- .getService(Components.interfaces.nsIObserverService);
-os.addObserver(observer, "private-browsing", true);
-os.addObserver(observer, "browser:purge-session-history", true);
-
-function readLastDirPref() {
- try {
- return prefSvc.getComplexValue(LAST_DIR_PREF, nsILocalFile);
- }
- catch (e) {
- return null;
- }
-}
-
-let gDownloadLastDirFile = readLastDirPref();
-let gDownloadLastDir = {
- get file() {
- if (gDownloadLastDirFile && !gDownloadLastDirFile.exists())
- gDownloadLastDirFile = null;
-
- if (pbSvc && pbSvc.privateBrowsingEnabled)
- return gDownloadLastDirFile;
- else
- return readLastDirPref();
- },
- set file(val) {
- if (pbSvc && pbSvc.privateBrowsingEnabled) {
- if (val instanceof Components.interfaces.nsIFile)
- gDownloadLastDirFile = val.clone();
- else
- gDownloadLastDirFile = null;
- } else {
- if (val instanceof Components.interfaces.nsIFile)
- prefSvc.setComplexValue(LAST_DIR_PREF, nsILocalFile, val);
- else if (prefSvc.prefHasUserValue(LAST_DIR_PREF))
- prefSvc.clearUserPref(LAST_DIR_PREF);
- }
- }
-};
deleted file mode 100644
--- a/toolkit/mozapps/downloads/src/DownloadUtils.jsm
+++ /dev/null
@@ -1,508 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Download Manager Utility Code.
- *
- * The Initial Developer of the Original Code is
- * Edward Lee <edward.lee@engineering.uiuc.edu>.
- * Portions created by the Initial Developer are Copyright (C) 2008
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-var EXPORTED_SYMBOLS = [ "DownloadUtils" ];
-
-/**
- * This module provides the DownloadUtils object which contains useful methods
- * for downloads such as displaying file sizes, transfer times, and download
- * locations.
- *
- * List of methods:
- *
- * [string status, double newLast]
- * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes,
- * [optional] double aSpeed, [optional] double aLastSec)
- *
- * string progress
- * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes)
- *
- * [string timeLeft, double newLast]
- * getTimeLeft(double aSeconds, [optional] double aLastSec)
- *
- * [string displayHost, string fullHost]
- * getURIHost(string aURIString)
- *
- * [double convertedBytes, string units]
- * convertByteUnits(int aBytes)
- *
- * [int time, string units, int subTime, string subUnits]
- * convertTimeUnits(double aSecs)
- */
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-
-__defineGetter__("PluralForm", function() {
- delete this.PluralForm;
- Cu.import("resource://gre/modules/PluralForm.jsm");
- return PluralForm;
-});
-
-const kDownloadProperties =
- "chrome://mozapps/locale/downloads/downloads.properties";
-
-// These strings will be converted to the corresponding ones from the string
-// bundle on use
-let kStrings = {
- statusFormat: "statusFormat2",
- transferSameUnits: "transferSameUnits",
- transferDiffUnits: "transferDiffUnits",
- transferNoTotal: "transferNoTotal",
- timePair: "timePair",
- timeLeftSingle: "timeLeftSingle",
- timeLeftDouble: "timeLeftDouble",
- timeFewSeconds: "timeFewSeconds",
- timeUnknown: "timeUnknown",
- doneScheme: "doneScheme",
- doneFileScheme: "doneFileScheme",
- units: ["bytes", "kilobyte", "megabyte", "gigabyte"],
- // Update timeSize in convertTimeUnits if changing the length of this array
- timeUnits: ["seconds", "minutes", "hours", "days"],
-};
-
-// This object will lazily load the strings defined in kStrings
-let gStr = {
- /**
- * Initialize lazy string getters
- */
- _init: function()
- {
- // Make each "name" a lazy-loading string that knows how to load itself. We
- // need to locally scope name and value to keep them around for the getter.
- for (let [name, value] in Iterator(kStrings))
- let ([n, v] = [name, value])
- gStr.__defineGetter__(n, function() gStr._getStr(n, v));
- },
-
- /**
- * Convert strings to those in the string bundle. This lazily loads the
- * string bundle *once* only when used the first time.
- */
- get _getStr()
- {
- // Delete the getter to be overwritten
- delete gStr._getStr;
-
- // Lazily load the bundle into the closure on first call to _getStr
- let getStr = Cc["@mozilla.org/intl/stringbundle;1"].
- getService(Ci.nsIStringBundleService).
- createBundle(kDownloadProperties).
- GetStringFromName;
-
- // _getStr is a function that sets string "name" to stringbundle's "value"
- return gStr._getStr = function(name, value) {
- // Delete the getter to be overwritten
- delete gStr[name];
-
- try {
- // "name" is a string or array of the stringbundle-loaded "value"
- return gStr[name] = typeof value == "string" ?
- getStr(value) :
- value.map(getStr);
- } catch (e) {
- log(["Couldn't get string '", name, "' from property '", value, "'"]);
- // Don't return anything (undefined), and because we deleted ourselves,
- // future accesses will also be undefined
- }
- };
- },
-};
-// Initialize the lazy string getters!
-gStr._init();
-
-// Keep track of at most this many second/lastSec pairs so that multiple calls
-// to getTimeLeft produce the same time left
-const kCachedLastMaxSize = 10;
-let gCachedLast = [];
-
-let DownloadUtils = {
- /**
- * Generate a full status string for a download given its current progress,
- * total size, speed, last time remaining
- *
- * @param aCurrBytes
- * Number of bytes transferred so far
- * @param [optional] aMaxBytes
- * Total number of bytes or -1 for unknown
- * @param [optional] aSpeed
- * Current transfer rate in bytes/sec or -1 for unknown
- * @param [optional] aLastSec
- * Last time remaining in seconds or Infinity for unknown
- * @return A pair: [download status text, new value of "last seconds"]
- */
- getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes,
- aSpeed, aLastSec)
- {
- if (aMaxBytes == null)
- aMaxBytes = -1;
- if (aSpeed == null)
- aSpeed = -1;
- if (aLastSec == null)
- aLastSec = Infinity;
-
- // Calculate the time remaining if we have valid values
- let seconds = (aSpeed > 0) && (aMaxBytes > 0) ?
- (aMaxBytes - aCurrBytes) / aSpeed : -1;
-
- // Update the bytes transferred and bytes total
- let status;
- let (transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes)) {
- // Insert 1 is the download progress
- status = replaceInsert(gStr.statusFormat, 1, transfer);
- }
-
- // Update the download rate
- let ([rate, unit] = DownloadUtils.convertByteUnits(aSpeed)) {
- // Insert 2 is the download rate
- status = replaceInsert(status, 2, rate);
- // Insert 3 is the |unit|/sec
- status = replaceInsert(status, 3, unit);
- }
-
- // Update time remaining
- let ([timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec)) {
- // Insert 4 is the time remaining
- status = replaceInsert(status, 4, timeLeft);
-
- return [status, newLast];
- }
- },
-
- /**
- * Generate the transfer progress string to show the current and total byte
- * size. Byte units will be as large as possible and the same units for
- * current and max will be supressed for the former.
- *
- * @param aCurrBytes
- * Number of bytes transferred so far
- * @param [optional] aMaxBytes
- * Total number of bytes or -1 for unknown
- * @return The transfer progress text
- */
- getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes)
- {
- if (aMaxBytes == null)
- aMaxBytes = -1;
-
- let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes);
- let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes);
-
- // Figure out which byte progress string to display
- let transfer;
- if (total < 0)
- transfer = gStr.transferNoTotal;
- else if (progressUnits == totalUnits)
- transfer = gStr.transferSameUnits;
- else
- transfer = gStr.transferDiffUnits;
-
- transfer = replaceInsert(transfer, 1, progress);
- transfer = replaceInsert(transfer, 2, progressUnits);
- transfer = replaceInsert(transfer, 3, total);
- transfer = replaceInsert(transfer, 4, totalUnits);
-
- return transfer;
- },
-
- /**
- * Generate a "time left" string given an estimate on the time left and the
- * last time. The extra time is used to give a better estimate on the time to
- * show. Both the time values are doubles instead of integers to help get
- * sub-second accuracy for current and future estimates.
- *
- * @param aSeconds
- * Current estimate on number of seconds left for the download
- * @param [optional] aLastSec
- * Last time remaining in seconds or Infinity for unknown
- * @return A pair: [time left text, new value of "last seconds"]
- */
- getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec)
- {
- if (aLastSec == null)
- aLastSec = Infinity;
-
- if (aSeconds < 0)
- return [gStr.timeUnknown, aLastSec];
-
- // Try to find a cached lastSec for the given second
- aLastSec = gCachedLast.reduce(function(aResult, aItem)
- aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec);
-
- // Add the current second/lastSec pair unless we have too many
- gCachedLast.push([aSeconds, aLastSec]);
- if (gCachedLast.length > kCachedLastMaxSize)
- gCachedLast.shift();
-
- // Apply smoothing only if the new time isn't a huge change -- e.g., if the
- // new time is more than half the previous time; this is useful for
- // downloads that start/resume slowly
- if (aSeconds > aLastSec / 2) {
- // Apply hysteresis to favor downward over upward swings
- // 30% of down and 10% of up (exponential smoothing)
- let (diff = aSeconds - aLastSec) {
- aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff;
- }
-
- // If the new time is similar, reuse something close to the last seconds,
- // but subtract a little to provide forward progress
- let diff = aSeconds - aLastSec;
- let diffPct = diff / aLastSec * 100;
- if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5)
- aSeconds = aLastSec - (diff < 0 ? .4 : .2);
- }
-
- // Decide what text to show for the time
- let timeLeft;
- if (aSeconds < 4) {
- // Be friendly in the last few seconds
- timeLeft = gStr.timeFewSeconds;
- } else {
- // Convert the seconds into its two largest units to display
- let [time1, unit1, time2, unit2] =
- DownloadUtils.convertTimeUnits(aSeconds);
-
- let pair1 = replaceInsert(gStr.timePair, 1, time1);
- pair1 = replaceInsert(pair1, 2, unit1);
- let pair2 = replaceInsert(gStr.timePair, 1, time2);
- pair2 = replaceInsert(pair2, 2, unit2);
-
- // Only show minutes for under 1 hour unless there's a few minutes left;
- // or the second pair is 0.
- if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) {
- timeLeft = replaceInsert(gStr.timeLeftSingle, 1, pair1);
- } else {
- // We've got 2 pairs of times to display
- timeLeft = replaceInsert(gStr.timeLeftDouble, 1, pair1);
- timeLeft = replaceInsert(timeLeft, 2, pair2);
- }
- }
-
- return [timeLeft, aSeconds];
- },
-
- /**
- * Get the appropriate display host string for a URI string depending on if
- * the URI has an eTLD + 1, is an IP address, a local file, or other protocol
- *
- * @param aURIString
- * The URI string to try getting an eTLD + 1, etc.
- * @return A pair: [display host for the URI string, full host name]
- */
- getURIHost: function DU_getURIHost(aURIString)
- {
- let ioService = Cc["@mozilla.org/network/io-service;1"].
- getService(Ci.nsIIOService);
- let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
- getService(Ci.nsIEffectiveTLDService);
- let idnService = Cc["@mozilla.org/network/idn-service;1"].
- getService(Ci.nsIIDNService);
-
- // Get a URI that knows about its components
- let uri = ioService.newURI(aURIString, null, null);
-
- // Get the inner-most uri for schemes like jar:
- if (uri instanceof Ci.nsINestedURI)
- uri = uri.innermostURI;
-
- let fullHost;
- try {
- // Get the full host name; some special URIs fail (data: jar:)
- fullHost = uri.host;
- } catch (e) {
- fullHost = "";
- }
-
- let displayHost;
- try {
- // This might fail if it's an IP address or doesn't have more than 1 part
- let baseDomain = eTLDService.getBaseDomain(uri);
-
- // Convert base domain for display; ignore the isAscii out param
- displayHost = idnService.convertToDisplayIDN(baseDomain, {});
- } catch (e) {
- // Default to the host name
- displayHost = fullHost;
- }
-
- // Check if we need to show something else for the host
- if (uri.scheme == "file") {
- // Display special text for file protocol
- displayHost = gStr.doneFileScheme;
- fullHost = displayHost;
- } else if (displayHost.length == 0) {
- // Got nothing; show the scheme (data: about: moz-icon:)
- displayHost = replaceInsert(gStr.doneScheme, 1, uri.scheme);
- fullHost = displayHost;
- } else if (uri.port != -1) {
- // Tack on the port if it's not the default port
- let port = ":" + uri.port;
- displayHost += port;
- fullHost += port;
- }
-
- return [displayHost, fullHost];
- },
-
- /**
- * Converts a number of bytes to the appropriate unit that results in a
- * number that needs fewer than 4 digits
- *
- * @param aBytes
- * Number of bytes to convert
- * @return A pair: [new value with 3 sig. figs., its unit]
- */
- convertByteUnits: function DU_convertByteUnits(aBytes)
- {
- let unitIndex = 0;
-
- // Convert to next unit if it needs 4 digits (after rounding), but only if
- // we know the name of the next unit
- while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) {
- aBytes /= 1024;
- unitIndex++;
- }
-
- // Get rid of insignificant bits by truncating to 1 or 0 decimal points
- // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
- aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) ? 1 : 0);
-
- return [aBytes, gStr.units[unitIndex]];
- },
-
- /**
- * Converts a number of seconds to the two largest units. Time values are
- * whole numbers, and units have the correct plural/singular form.
- *
- * @param aSecs
- * Seconds to convert into the appropriate 2 units
- * @return 4-item array [first value, its unit, second value, its unit]
- */
- convertTimeUnits: function DU_convertTimeUnits(aSecs)
- {
- // These are the maximum values for seconds, minutes, hours corresponding
- // with gStr.timeUnits without the last item
- let timeSize = [60, 60, 24];
-
- let time = aSecs;
- let scale = 1;
- let unitIndex = 0;
-
- // Keep converting to the next unit while we have units left and the
- // current one isn't the largest unit possible
- while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) {
- time /= timeSize[unitIndex];
- scale *= timeSize[unitIndex];
- unitIndex++;
- }
-
- let value = convertTimeUnitsValue(time);
- let units = convertTimeUnitsUnits(value, unitIndex);
-
- let extra = aSecs - value * scale;
- let nextIndex = unitIndex - 1;
-
- // Convert the extra time to the next largest unit
- for (let index = 0; index < nextIndex; index++)
- extra /= timeSize[index];
-
- let value2 = convertTimeUnitsValue(extra);
- let units2 = convertTimeUnitsUnits(value2, nextIndex);
-
- return [value, units, value2, units2];
- },
-};
-
-/**
- * Private helper for convertTimeUnits that gets the display value of a time
- *
- * @param aTime
- * Time value for display
- * @return An integer value for the time rounded down
- */
-function convertTimeUnitsValue(aTime)
-{
- return Math.floor(aTime);
-}
-
-/**
- * Private helper for convertTimeUnits that gets the display units of a time
- *
- * @param aTime
- * Time value for display
- * @param aIndex
- * Index into gStr.timeUnits for the appropriate unit
- * @return The appropriate plural form of the unit for the time
- */
-function convertTimeUnitsUnits(aTime, aIndex)
-{
- // Negative index would be an invalid unit, so just give empty
- if (aIndex < 0)
- return "";
-
- return PluralForm.get(aTime, gStr.timeUnits[aIndex]);
-}
-
-/**
- * Private helper function to replace a placeholder string with a real string
- *
- * @param aText
- * Source text containing placeholder (e.g., #1)
- * @param aIndex
- * Index number of placeholder to replace
- * @param aValue
- * New string to put in place of placeholder
- * @return The string with placeholder replaced with the new string
- */
-function replaceInsert(aText, aIndex, aValue)
-{
- return aText.replace("#" + aIndex, aValue);
-}
-
-/**
- * Private helper function to log errors to the error console and command line
- *
- * @param aMsg
- * Error message to log or an array of strings to concat
- */
-function log(aMsg)
-{
- let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg);
- Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).
- logStringMessage(msg);
- dump(msg + "\n");
-}
deleted file mode 100644
--- a/toolkit/mozapps/downloads/src/Makefile.in
+++ /dev/null
@@ -1,59 +0,0 @@
-#
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is mozilla.org code.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
-
-DEPTH = ../../../..
-topsrcdir = @top_srcdir@
-srcdir = @srcdir@
-VPATH = @srcdir@
-
-include $(DEPTH)/config/autoconf.mk
-
-MODULE = helperAppDlg
-
-EXTRA_COMPONENTS = nsHelperAppDlg.js
-GARBAGE += nsHelperAppDlg.js
-
-EXTRA_JS_MODULES = \
- DownloadUtils.jsm \
- DownloadLastDir.jsm \
- $(NULL)
-
-include $(topsrcdir)/config/rules.mk
-
-nsHelperAppDlg.js: nsHelperAppDlg.js.in
- $(PYTHON) $(MOZILLA_DIR)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $^ > $@
-
deleted file mode 100644
--- a/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in
+++ /dev/null
@@ -1,1238 +0,0 @@
-/*
-# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla.org Code.
-#
-# The Initial Developer of the Original Code is
-# Doron Rosenberg.
-# Portions created by the Initial Developer are Copyright (C) 2001
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-# Bill Law <law@netscape.com>
-# Scott MacGregor <mscott@netscape.com>
-# Ben Goodger <ben@bengoodger.com> (2.0)
-# Fredrik Holmqvist <thesuckiestemail@yahoo.se>
-# Dan Mosedale <dmose@mozilla.org>
-# Jim Mathies <jmathies@mozilla.com>
-# Ehsan Akhgari <ehsan.akhgari@gmail.com>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
-*/
-
-///////////////////////////////////////////////////////////////////////////////
-//// Helper Functions
-
-/**
- * Determines if a given directory is able to be used to download to.
- *
- * @param aDirectory
- * The directory to check.
- * @returns true if we can use the directory, false otherwise.
- */
-function isUsableDirectory(aDirectory)
-{
- return aDirectory.exists() && aDirectory.isDirectory() &&
- aDirectory.isWritable();
-}
-
-///////////////////////////////////////////////////////////////////////////////
-//// nsUnkownContentTypeDialog
-
-/* This file implements the nsIHelperAppLauncherDialog interface.
- *
- * The implementation consists of a JavaScript "class" named nsUnknownContentTypeDialog,
- * comprised of:
- * - a JS constructor function
- * - a prototype providing all the interface methods and implementation stuff
- *
- * In addition, this file implements an nsIModule object that registers the
- * nsUnknownContentTypeDialog component.
- */
-
-const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
-const nsITimer = Components.interfaces.nsITimer;
-
-Components.utils.import("resource://gre/modules/DownloadLastDir.jsm");
-
-/* ctor
- */
-function nsUnknownContentTypeDialog() {
- // Initialize data properties.
- this.mLauncher = null;
- this.mContext = null;
- this.mSourcePath = null;
- this.chosenApp = null;
- this.givenDefaultApp = false;
- this.updateSelf = true;
- this.mTitle = "";
-}
-
-nsUnknownContentTypeDialog.prototype = {
- nsIMIMEInfo : Components.interfaces.nsIMIMEInfo,
-
- QueryInterface: function (iid) {
- if (!iid.equals(Components.interfaces.nsIHelperAppLauncherDialog) &&
- !iid.equals(Components.interfaces.nsITimerCallback) &&
- !iid.equals(Components.interfaces.nsISupports)) {
- throw Components.results.NS_ERROR_NO_INTERFACE;
- }
- return this;
- },
-
- // ---------- nsIHelperAppLauncherDialog methods ----------
-
- // show: Open XUL dialog using window watcher. Since the dialog is not
- // modal, it needs to be a top level window and the way to open
- // one of those is via that route).
- show: function(aLauncher, aContext, aReason) {
- this.mLauncher = aLauncher;
- this.mContext = aContext;
-
- const nsITimer = Components.interfaces.nsITimer;
- this._showTimer = Components.classes["@mozilla.org/timer;1"]
- .createInstance(nsITimer);
- this._showTimer.initWithCallback(this, 0, nsITimer.TYPE_ONE_SHOT);
- },
-
- // When opening from new tab, if tab closes while dialog is opening,
- // (which is a race condition on the XUL file being cached and the timer
- // in nsExternalHelperAppService), the dialog gets a blur and doesn't
- // activate the OK button. So we wait a bit before doing opening it.
- reallyShow: function() {
- try {
- var ir = this.mContext.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
- var dwi = ir.getInterface(Components.interfaces.nsIDOMWindowInternal);
- var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
- .getService(Components.interfaces.nsIWindowWatcher);
- this.mDialog = ww.openWindow(dwi,
- "chrome://mozapps/content/downloads/unknownContentType.xul",
- null,
- "chrome,centerscreen,titlebar,dialog=yes,dependent",
- null);
- } catch (ex) {
- // The containing window may have gone away. Break reference
- // cycles and stop doing the download.
- const NS_BINDING_ABORTED = 0x804b0002;
- this.mLauncher.cancel(NS_BINDING_ABORTED);
- return;
- }
-
- // Hook this object to the dialog.
- this.mDialog.dialog = this;
-
- // Hook up utility functions.
- this.getSpecialFolderKey = this.mDialog.getSpecialFolderKey;
-
- // Watch for error notifications.
- this.progressListener.helperAppDlg = this;
- this.mLauncher.setWebProgressListener(this.progressListener);
- },
-
- // promptForSaveToFile: Display file picker dialog and return selected file.
- // This is called by the External Helper App Service
- // after the ucth dialog calls |saveToDisk| with a null
- // target filename (no target, therefore user must pick).
- //
- // Alternatively, if the user has selected to have all
- // files download to a specific location, return that
- // location and don't ask via the dialog.
- //
- // Note - this function is called without a dialog, so it cannot access any part
- // of the dialog XUL as other functions on this object do.
- promptForSaveToFile: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) {
- var result = null;
-
- this.mLauncher = aLauncher;
-
- let prefs = Components.classes["@mozilla.org/preferences-service;1"]
- .getService(Components.interfaces.nsIPrefBranch);
- let bundle = Components.classes["@mozilla.org/intl/stringbundle;1"].
- getService(Components.interfaces.nsIStringBundleService).
- createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties");
-
- if (!aForcePrompt) {
- // Check to see if the user wishes to auto save to the default download
- // folder without prompting. Note that preference might not be set.
- let autodownload = false;
- try {
- autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR);
- } catch (e) { }
-
- if (autodownload) {
- // Retrieve the user's default download directory
- let dnldMgr = Components.classes["@mozilla.org/download-manager;1"]
- .getService(Components.interfaces.nsIDownloadManager);
- let defaultFolder = dnldMgr.userDownloadsDirectory;
-
- try {
- result = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExtension);
- }
- catch (ex) {
- if (ex.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED) {
- let prompter = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
- getService(Components.interfaces.nsIPromptService);
-
- // Display error alert (using text supplied by back-end)
- prompter.alert(this.dialog,
- bundle.GetStringFromName("badPermissions.title"),
- bundle.GetStringFromName("badPermissions"));
-
- return;
- }
- }
-
- // Check to make sure we have a valid directory, otherwise, prompt
- if (result)
- return result;
- }
- }
-
- // Use file picker to show dialog.
- var nsIFilePicker = Components.interfaces.nsIFilePicker;
- var picker = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
- var windowTitle = bundle.GetStringFromName("saveDialogTitle");
- var parent = aContext.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowInternal);
- picker.init(parent, windowTitle, nsIFilePicker.modeSave);
- picker.defaultString = aDefaultFile;
-
- if (aSuggestedFileExtension) {
- // aSuggestedFileExtension includes the period, so strip it
- picker.defaultExtension = aSuggestedFileExtension.substring(1);
- }
- else {
- try {
- picker.defaultExtension = this.mLauncher.MIMEInfo.primaryExtension;
- }
- catch (ex) { }
- }
-
- var wildCardExtension = "*";
- if (aSuggestedFileExtension) {
- wildCardExtension += aSuggestedFileExtension;
- picker.appendFilter(this.mLauncher.MIMEInfo.description, wildCardExtension);
- }
-
- picker.appendFilters( nsIFilePicker.filterAll );
-
- // Default to lastDir if it is valid, otherwise use the user's default
- // downloads directory. userDownloadsDirectory should always return a
- // valid directory, so we can safely default to it.
- var dnldMgr = Components.classes["@mozilla.org/download-manager;1"]
- .getService(Components.interfaces.nsIDownloadManager);
- picker.displayDirectory = dnldMgr.userDownloadsDirectory;
-
- // The last directory preference may not exist, which will throw.
- try {
- var lastDir = gDownloadLastDir.file;
- if (isUsableDirectory(lastDir))
- picker.displayDirectory = lastDir;
- }
- catch (ex) {
- }
-
- if (picker.show() == nsIFilePicker.returnCancel) {
- // null result means user cancelled.
- return null;
- }
-
- // Be sure to save the directory the user chose through the Save As...
- // dialog as the new browser.download.dir since the old one
- // didn't exist.
- result = picker.file;
-
- if (result) {
- try {
- // Remove the file so that it's not there when we ensure non-existence later;
- // this is safe because for the file to exist, the user would have had to
- // confirm that he wanted the file overwritten.
- if (result.exists())
- result.remove(false);
- }
- catch (e) { }
- var newDir = result.parent.QueryInterface(Components.interfaces.nsILocalFile);
-
- // Do not store the last save directory as a pref inside the private browsing mode
- gDownloadLastDir.file = newDir;
-
- result = this.validateLeafName(newDir, result.leafName, null);
- }
- return result;
- },
-
- /**
- * Ensures that a local folder/file combination does not already exist in
- * the file system (or finds such a combination with a reasonably similar
- * leaf name), creates the corresponding file, and returns it.
- *
- * @param aLocalFile
- * the folder where the file resides
- * @param aLeafName
- * the string name of the file (may be empty if no name is known,
- * in which case a name will be chosen)
- * @param aFileExt
- * the extension of the file, if one is known; this will be ignored
- * if aLeafName is non-empty
- * @returns nsILocalFile
- * the created file
- */
- validateLeafName: function (aLocalFile, aLeafName, aFileExt)
- {
- if (!(aLocalFile && isUsableDirectory(aLocalFile)))
- return null;
-
- // Remove any leading periods, since we don't want to save hidden files
- // automatically.
- aLeafName = aLeafName.replace(/^\.+/, "");
-
- if (aLeafName == "")
- aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
- aLocalFile.append(aLeafName);
-
- this.makeFileUnique(aLocalFile);
-
-#ifdef XP_WIN
- let ext;
- try {
- // We can fail here if there's no primary extension set
- ext = "." + this.mLauncher.MIMEInfo.primaryExtension;
- } catch (e) { }
-
- // Append a file extension if it's an executable that doesn't have one
- // but make sure we actually have an extension to add
- let leaf = aLocalFile.leafName;
- if (aLocalFile.isExecutable() && ext &&
- leaf.substring(leaf.length - ext.length) != ext) {
- let f = aLocalFile.clone();
- aLocalFile.leafName = leaf + ext;
-
- f.remove(false);
- this.makeFileUnique(aLocalFile);
- }
-#endif
-
- return aLocalFile;
- },
-
- /**
- * Generates and returns a uniquely-named file from aLocalFile. If
- * aLocalFile does not exist, it will be the file returned; otherwise, a
- * file whose name is similar to that of aLocalFile will be returned.
- */
- makeFileUnique: function (aLocalFile)
- {
- try {
- // Note - this code is identical to that in
- // toolkit/content/contentAreaUtils.js.
- // If you are updating this code, update that code too! We can't share code
- // here since this is called in a js component.
- var collisionCount = 0;
- while (aLocalFile.exists()) {
- collisionCount++;
- if (collisionCount == 1) {
- // Append "(2)" before the last dot in (or at the end of) the filename
- // special case .ext.gz etc files so we don't wind up with .tar(2).gz
- if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) {
- aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
- }
- else {
- aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
- }
- }
- else {
- // replace the last (n) in the filename with (n+1)
- aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")");
- }
- }
- aLocalFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0600);
- }
- catch (e) {
- dump("*** exception in validateLeafName: " + e + "\n");
-
- if (e.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED)
- throw e;
-
- if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) {
- aLocalFile.append("unnamed");
- if (aLocalFile.exists())
- aLocalFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0600);
- }
- }
- },
-
- // ---------- implementation methods ----------
-
- // Web progress listener so we can detect errors while mLauncher is
- // streaming the data to a temporary file.
- progressListener: {
- // Implementation properties.
- helperAppDlg: null,
-
- // nsIWebProgressListener methods.
- // Look for error notifications and display alert to user.
- onStatusChange: function( aWebProgress, aRequest, aStatus, aMessage ) {
- if ( aStatus != Components.results.NS_OK ) {
- // Get prompt service.
- var prompter = Components.classes[ "@mozilla.org/embedcomp/prompt-service;1" ]
- .getService( Components.interfaces.nsIPromptService );
- // Display error alert (using text supplied by back-end).
- prompter.alert( this.dialog, this.helperAppDlg.mTitle, aMessage );
-
- // Close the dialog.
- this.helperAppDlg.onCancel();
- if ( this.helperAppDlg.mDialog ) {
- this.helperAppDlg.mDialog.close();
- }
- }
- },
-
- // Ignore onProgressChange, onProgressChange64, onStateChange, onLocationChange, onSecurityChange, and onRefreshAttempted notifications.
- onProgressChange: function( aWebProgress,
- aRequest,
- aCurSelfProgress,
- aMaxSelfProgress,
- aCurTotalProgress,
- aMaxTotalProgress ) {
- },
-
- onProgressChange64: function( aWebProgress,
- aRequest,
- aCurSelfProgress,
- aMaxSelfProgress,
- aCurTotalProgress,
- aMaxTotalProgress ) {
- },
-
-
-
- onStateChange: function( aWebProgress, aRequest, aStateFlags, aStatus ) {
- },
-
- onLocationChange: function( aWebProgress, aRequest, aLocation ) {
- },
-
- onSecurityChange: function( aWebProgress, aRequest, state ) {
- },
-
- onRefreshAttempted: function( aWebProgress, aURI, aDelay, aSameURI ) {
- return true;
- }
- },
-
- // initDialog: Fill various dialog fields with initial content.
- initDialog : function() {
- // Put file name in window title.
- var suggestedFileName = this.mLauncher.suggestedFileName;
-
- // Some URIs do not implement nsIURL, so we can't just QI.
- var url = this.mLauncher.source;
- var fname = "";
- this.mSourcePath = url.prePath;
- try {
- url = url.QueryInterface( Components.interfaces.nsIURL );
- // A url, use file name from it.
- fname = url.fileName;
- this.mSourcePath += url.directory;
- } catch (ex) {
- // A generic uri, use path.
- fname = url.path;
- this.mSourcePath += url.path;
- }
-
- if (suggestedFileName)
- fname = suggestedFileName;
-
- var displayName = fname.replace(/ +/g, " ");
-
- this.mTitle = this.dialogElement("strings").getFormattedString("title", [displayName]);
- this.mDialog.document.title = this.mTitle;
-
- // Put content type, filename and location into intro.
- this.initIntro(url, fname, displayName);
-
- var iconString = "moz-icon://" + fname + "?size=16&contentType=" + this.mLauncher.MIMEInfo.MIMEType;
- this.dialogElement("contentTypeImage").setAttribute("src", iconString);
-
- // if always-save and is-executable and no-handler
- // then set up simple ui
- var mimeType = this.mLauncher.MIMEInfo.MIMEType;
- var shouldntRememberChoice = (mimeType == "application/octet-stream" ||
- mimeType == "application/x-msdownload" ||
- this.mLauncher.targetFileIsExecutable);
- if (shouldntRememberChoice && !this.openWithDefaultOK()) {
- // hide featured choice
- this.dialogElement("normalBox").collapsed = true;
- // show basic choice
- this.dialogElement("basicBox").collapsed = false;
- // change button labels and icons; use "save" icon for the accept
- // button since it's the only action possible
- let acceptButton = this.mDialog.document.documentElement
- .getButton("accept");
- acceptButton.label = this.dialogElement("strings")
- .getString("unknownAccept.label");
- acceptButton.setAttribute("icon", "save");
- this.mDialog.document.documentElement.getButton("cancel").label = this.dialogElement("strings").getString("unknownCancel.label");
- // hide other handler
- this.dialogElement("openHandler").collapsed = true;
- // set save as the selected option
- this.dialogElement("mode").selectedItem = this.dialogElement("save");
- }
- else {
- this.initAppAndSaveToDiskValues();
-
- // Initialize "always ask me" box. This should always be disabled
- // and set to true for the ambiguous type application/octet-stream.
- // We don't also check for application/x-msdownload here since we
- // want users to be able to autodownload .exe files.
- var rememberChoice = this.dialogElement("rememberChoice");
-
-#if 0
- // Just because we have a content-type of application/octet-stream
- // here doesn't actually mean that the content is of that type. Many
- // servers default to sending text/plain for file types they don't know
- // about. To account for this, the uriloader does some checking to see
- // if a file sent as text/plain contains binary characters, and if so (*)
- // it morphs the content-type into application/octet-stream so that
- // the file can be properly handled. Since this is not generic binary
- // data, rather, a data format that the system probably knows about,
- // we don't want to use the content-type provided by this dialog's
- // opener, as that's the generic application/octet-stream that the
- // uriloader has passed, rather we want to ask the MIME Service.
- // This is so we don't needlessly disable the "autohandle" checkbox.
- var mimeService = Components.classes["@mozilla.org/mime;1"].getService(Components.interfaces.nsIMIMEService);
- var type = mimeService.getTypeFromURI(this.mLauncher.source);
- this.realMIMEInfo = mimeService.getFromTypeAndExtension(type, "");
-
- if (type == "application/octet-stream") {
-#endif
- if (shouldntRememberChoice) {
- rememberChoice.checked = false;
- rememberChoice.disabled = true;
- }
- else {
- rememberChoice.checked = !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling;
- }
- this.toggleRememberChoice(rememberChoice);
-
- // XXXben - menulist won't init properly, hack.
- var openHandler = this.dialogElement("openHandler");
- openHandler.parentNode.removeChild(openHandler);
- var openHandlerBox = this.dialogElement("openHandlerBox");
- openHandlerBox.appendChild(openHandler);
- }
-
- this.mDialog.setTimeout("dialog.postShowCallback()", 0);
-
- this.mDialog.document.documentElement.getButton("accept").disabled = true;
- this._showTimer = Components.classes["@mozilla.org/timer;1"]
- .createInstance(nsITimer);
- this._showTimer.initWithCallback(this, 250, nsITimer.TYPE_ONE_SHOT);
- },
-
- notify: function (aTimer) {
- if (aTimer == this._showTimer) {
- if (!this.mDialog) {
- this.reallyShow();
- } else {
- // The user may have already canceled the dialog.
- try {
- if (!this._blurred) {
- this.mDialog.document.documentElement.getButton("accept").disabled = false;
- }
- } catch (ex) {}
- this._delayExpired = true;
- }
- // The timer won't release us, so we have to release it.
- this._showTimer = null;
- }
- else if (aTimer == this._saveToDiskTimer) {
- // Since saveToDisk may open a file picker and therefore block this routine,
- // we should only call it once the dialog is closed.
- this.mLauncher.saveToDisk(null, false);
- this._saveToDiskTimer = null;
- }
- },
-
- postShowCallback: function () {
- this.mDialog.sizeToContent();
-
- // Set initial focus
- this.dialogElement("mode").focus();
- },
-
- // initIntro:
- initIntro: function(url, filename, displayname) {
- this.dialogElement( "location" ).value = displayname;
- this.dialogElement( "location" ).setAttribute("realname", filename);
- this.dialogElement( "location" ).setAttribute("tooltiptext", displayname);
-
- // if mSourcePath is a local file, then let's use the pretty path name instead of an ugly
- // url...
- var pathString = this.mSourcePath;
- try
- {
- var fileURL = url.QueryInterface(Components.interfaces.nsIFileURL);
- if (fileURL)
- {
- var fileObject = fileURL.file;
- if (fileObject)
- {
- var parentObject = fileObject.parent;
- if (parentObject)
- {
- pathString = parentObject.path;
- }
- }
- }
- } catch(ex) {}
-
- if (pathString == this.mSourcePath)
- {
- // wasn't a fileURL
- var tmpurl = url.clone(); // don't want to change the real url
- try {
- tmpurl.userPass = "";
- } catch (ex) {}
- pathString = tmpurl.prePath;
- }
-
- // Set the location text, which is separate from the intro text so it can be cropped
- var location = this.dialogElement( "source" );
- location.value = pathString;
- location.setAttribute("tooltiptext", this.mSourcePath);
-
- // Show the type of file.
- var type = this.dialogElement("type");
- var mimeInfo = this.mLauncher.MIMEInfo;
-
- // 1. Try to use the pretty description of the type, if one is available.
- var typeString = mimeInfo.description;
-
- if (typeString == "") {
- // 2. If there is none, use the extension to identify the file, e.g. "ZIP file"
- var primaryExtension = "";
- try {
- primaryExtension = mimeInfo.primaryExtension;
- }
- catch (ex) {
- }
- if (primaryExtension != "")
- typeString = this.dialogElement("strings").getFormattedString("fileType", [primaryExtension.toUpperCase()]);
- // 3. If we can't even do that, just give up and show the MIME type.
- else
- typeString = mimeInfo.MIMEType;
- }
-
- type.value = typeString;
- },
-
- _blurred: false,
- _delayExpired: false,
- onBlur: function(aEvent) {
- this._blurred = true;
- this.mDialog.document.documentElement.getButton("accept").disabled = true;
- },
-
- onFocus: function(aEvent) {
- this._blurred = false;
- if (this._delayExpired) {
- var script = "document.documentElement.getButton('accept').disabled = false";
- this.mDialog.setTimeout(script, 250);
- }
- },
-
- // Returns true if opening the default application makes sense.
- openWithDefaultOK: function() {
- // The checking is different on Windows...
-#ifdef XP_WIN
- // Windows presents some special cases.
- // We need to prevent use of "system default" when the file is
- // executable (so the user doesn't launch nasty programs downloaded
- // from the web), and, enable use of "system default" if it isn't
- // executable (because we will prompt the user for the default app
- // in that case).
-
- // Default is Ok if the file isn't executable (and vice-versa).
- return !this.mLauncher.targetFileIsExecutable;
-#else
- // On other platforms, default is Ok if there is a default app.
- // Note that nsIMIMEInfo providers need to ensure that this holds true
- // on each platform.
- return this.mLauncher.MIMEInfo.hasDefaultHandler;
-#endif
- },
-
- // Set "default" application description field.
- initDefaultApp: function() {
- // Use description, if we can get one.
- var desc = this.mLauncher.MIMEInfo.defaultDescription;
- if (desc) {
- var defaultApp = this.dialogElement("strings").getFormattedString("defaultApp", [desc]);
- this.dialogElement("defaultHandler").label = defaultApp;
- }
- else {
- this.dialogElement("modeDeck").setAttribute("selectedIndex", "1");
- // Hide the default handler item too, in case the user picks a
- // custom handler at a later date which triggers the menulist to show.
- this.dialogElement("defaultHandler").hidden = true;
- }
- },
-
- // getPath:
- getPath: function (aFile) {
-#ifdef XP_MACOSX
- return aFile.leafName || aFile.path;
-#else
- return aFile.path;
-#endif
- },
-
- // initAppAndSaveToDiskValues:
- initAppAndSaveToDiskValues: function() {
- var modeGroup = this.dialogElement("mode");
-
- // We don't let users open .exe files or random binary data directly
- // from the browser at the moment because of security concerns.
- var openWithDefaultOK = this.openWithDefaultOK();
- var mimeType = this.mLauncher.MIMEInfo.MIMEType;
- if (this.mLauncher.targetFileIsExecutable || (
- (mimeType == "application/octet-stream" ||
- mimeType == "application/x-msdownload") &&
- !openWithDefaultOK)) {
- this.dialogElement("open").disabled = true;
- var openHandler = this.dialogElement("openHandler");
- openHandler.disabled = true;
- openHandler.selectedItem = null;
- modeGroup.selectedItem = this.dialogElement("save");
- return;
- }
-
- // Fill in helper app info, if there is any.
- try {
- this.chosenApp =
- this.mLauncher.MIMEInfo.preferredApplicationHandler
- .QueryInterface(Components.interfaces.nsILocalHandlerApp);
- } catch (e) {
- this.chosenApp = null;
- }
- // Initialize "default application" field.
- this.initDefaultApp();
-
- var otherHandler = this.dialogElement("otherHandler");
-
- // Fill application name textbox.
- if (this.chosenApp && this.chosenApp.executable &&
- this.chosenApp.executable.path) {
- otherHandler.setAttribute("path",
- this.getPath(this.chosenApp.executable));
-
-#if XP_MACOSX
- this.chosenApp.executable.QueryInterface(Components.interfaces.nsILocalFileMac);
- otherHandler.label = this.chosenApp.executable.bundleDisplayName;
-#else
- otherHandler.label = this.chosenApp.executable.leafName;
-#endif
- otherHandler.hidden = false;
- }
-
- var useDefault = this.dialogElement("useSystemDefault");
- var openHandler = this.dialogElement("openHandler");
- openHandler.selectedIndex = 0;
-
- if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useSystemDefault) {
- // Open (using system default).
- modeGroup.selectedItem = this.dialogElement("open");
- } else if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useHelperApp) {
- // Open with given helper app.
- modeGroup.selectedItem = this.dialogElement("open");
- openHandler.selectedIndex = 1;
- } else {
- // Save to disk.
- modeGroup.selectedItem = this.dialogElement("save");
- }
-
- // If we don't have a "default app" then disable that choice.
- if (!openWithDefaultOK) {
- var useDefault = this.dialogElement("defaultHandler");
- var isSelected = useDefault.selected;
-
- // Disable that choice.
- useDefault.hidden = true;
- // If that's the default, then switch to "save to disk."
- if (isSelected) {
- openHandler.selectedIndex = 1;
- modeGroup.selectedItem = this.dialogElement("save");
- }
- }
-
- otherHandler.nextSibling.hidden = otherHandler.nextSibling.nextSibling.hidden = false;
- this.updateOKButton();
- },
-
- // Returns the user-selected application
- helperAppChoice: function() {
- return this.chosenApp;
- },
-
- get saveToDisk() {
- return this.dialogElement("save").selected;
- },
-
- get useOtherHandler() {
- return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 1;
- },
-
- get useSystemDefault() {
- return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 0;
- },
-
- toggleRememberChoice: function (aCheckbox) {
- this.dialogElement("settingsChange").hidden = !aCheckbox.checked;
- this.mDialog.sizeToContent();
- },
-
- openHandlerCommand: function () {
- var openHandler = this.dialogElement("openHandler");
- if (openHandler.selectedItem.id == "choose")
- this.chooseApp();
- else
- openHandler.setAttribute("lastSelectedItemID", openHandler.selectedItem.id);
- },
-
- updateOKButton: function() {
- var ok = false;
- if (this.dialogElement("save").selected) {
- // This is always OK.
- ok = true;
- }
- else if (this.dialogElement("open").selected) {
- switch (this.dialogElement("openHandler").selectedIndex) {
- case 0:
- // No app need be specified in this case.
- ok = true;
- break;
- case 1:
- // only enable the OK button if we have a default app to use or if
- // the user chose an app....
- ok = this.chosenApp || /\S/.test(this.dialogElement("otherHandler").getAttribute("path"));
- break;
- }
- }
-
- // Enable Ok button if ok to press.
- this.mDialog.document.documentElement.getButton("accept").disabled = !ok;
- },
-
- // Returns true iff the user-specified helper app has been modified.
- appChanged: function() {
- return this.helperAppChoice() != this.mLauncher.MIMEInfo.preferredApplicationHandler;
- },
-
- updateMIMEInfo: function() {
- var needUpdate = false;
- // If current selection differs from what's in the mime info object,
- // then we need to update.
- if (this.saveToDisk) {
- needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.saveToDisk;
- if (needUpdate)
- this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.saveToDisk;
- }
- else if (this.useSystemDefault) {
- needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useSystemDefault;
- if (needUpdate)
- this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useSystemDefault;
- }
- else {
- // For "open with", we need to check both preferred action and whether the user chose
- // a new app.
- needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useHelperApp || this.appChanged();
- if (needUpdate) {
- this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useHelperApp;
- // App may have changed - Update application
- var app = this.helperAppChoice();
- this.mLauncher.MIMEInfo.preferredApplicationHandler = app;
- }
- }
- // We will also need to update if the "always ask" flag has changed.
- needUpdate = needUpdate || this.mLauncher.MIMEInfo.alwaysAskBeforeHandling != (!this.dialogElement("rememberChoice").checked);
-
- // One last special case: If the input "always ask" flag was false, then we always
- // update. In that case we are displaying the helper app dialog for the first
- // time for this mime type and we need to store the user's action in the mimeTypes.rdf
- // data source (whether that action has changed or not; if it didn't change, then we need
- // to store the "always ask" flag so the helper app dialog will or won't display
- // next time, per the user's selection).
- needUpdate = needUpdate || !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling;
-
- // Make sure mime info has updated setting for the "always ask" flag.
- this.mLauncher.MIMEInfo.alwaysAskBeforeHandling = !this.dialogElement("rememberChoice").checked;
-
- return needUpdate;
- },
-
- // See if the user changed things, and if so, update the
- // mimeTypes.rdf entry for this mime type.
- updateHelperAppPref: function() {
- var ha = new this.mDialog.HelperApps();
- ha.updateTypeInfo(this.mLauncher.MIMEInfo);
- ha.destroy();
- },
-
- // onOK:
- onOK: function() {
- // Verify typed app path, if necessary.
- if (this.useOtherHandler) {
- var helperApp = this.helperAppChoice();
- if (!helperApp || !helperApp.executable ||
- !helperApp.executable.exists()) {
- // Show alert and try again.
- var bundle = this.dialogElement("strings");
- var msg = bundle.getFormattedString("badApp", [this.dialogElement("otherHandler").getAttribute("path")]);
- var svc = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService);
- svc.alert(this.mDialog, bundle.getString("badApp.title"), msg);
-
- // Disable the OK button.
- this.mDialog.document.documentElement.getButton("accept").disabled = true;
- this.dialogElement("mode").focus();
-
- // Clear chosen application.
- this.chosenApp = null;
-
- // Leave dialog up.
- return false;
- }
- }
-
- // Remove our web progress listener (a progress dialog will be
- // taking over).
- this.mLauncher.setWebProgressListener(null);
-
- // saveToDisk and launchWithApplication can return errors in
- // certain circumstances (e.g. The user clicks cancel in the
- // "Save to Disk" dialog. In those cases, we don't want to
- // update the helper application preferences in the RDF file.
- try {
- var needUpdate = this.updateMIMEInfo();
-
- if (this.dialogElement("save").selected) {
- // If we're using a default download location, create a path
- // for the file to be saved to to pass to |saveToDisk| - otherwise
- // we must ask the user to pick a save name.
-
-#if 0
- var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
- var targetFile = null;
- try {
- targetFile = prefs.getComplexValue("browser.download.defaultFolder",
- Components.interfaces.nsILocalFile);
- var leafName = this.dialogElement("location").getAttribute("realname");
- // Ensure that we don't overwrite any existing files here.
- targetFile = this.validateLeafName(targetFile, leafName, null);
- }
- catch(e) { }
-
- this.mLauncher.saveToDisk(targetFile, false);
-#endif
-
- // see @notify
- // we cannot use opener's setTimeout, see bug 420405
- this._saveToDiskTimer = Components.classes["@mozilla.org/timer;1"]
- .createInstance(nsITimer);
- this._saveToDiskTimer.initWithCallback(this, 0,
- nsITimer.TYPE_ONE_SHOT);
- }
- else
- this.mLauncher.launchWithApplication(null, false);
-
- // Update user pref for this mime type (if necessary). We do not
- // store anything in the mime type preferences for the ambiguous
- // type application/octet-stream. We do NOT do this for
- // application/x-msdownload since we want users to be able to
- // autodownload these to disk.
- if (needUpdate && this.mLauncher.MIMEInfo.MIMEType != "application/octet-stream")
- this.updateHelperAppPref();
- } catch(e) { }
-
- // Unhook dialog from this object.
- this.mDialog.dialog = null;
-
- // Close up dialog by returning true.
- return true;
- },
-
- // onCancel:
- onCancel: function() {
- // Remove our web progress listener.
- this.mLauncher.setWebProgressListener(null);
-
- // Cancel app launcher.
- try {
- const NS_BINDING_ABORTED = 0x804b0002;
- this.mLauncher.cancel(NS_BINDING_ABORTED);
- } catch(exception) {
- }
-
- // Unhook dialog from this object.
- this.mDialog.dialog = null;
-
- // Close up dialog by returning true.
- return true;
- },
-
- // dialogElement: Convenience.
- dialogElement: function(id) {
- return this.mDialog.document.getElementById(id);
- },
-
- // Retrieve the pretty description from the file
- getFileDisplayName: function getFileDisplayName(file)
- {
-#ifdef XP_WIN
- if (file instanceof Components.interfaces.nsILocalFileWin) {
- try {
- return file.getVersionInfoField("FileDescription");
- } catch (ex) {
- }
- }
-#endif
- return file.leafName;
- },
-
- // chooseApp: Open file picker and prompt user for application.
- chooseApp: function() {
-#ifdef XP_WIN
- // Protect against the lack of an extension
- var fileExtension = "";
- try {
- fileExtension = this.mLauncher.MIMEInfo.primaryExtension;
- } catch(ex) {
- }
-
- // Try to use the pretty description of the type, if one is available.
- var typeString = this.mLauncher.MIMEInfo.description;
-
- if (!typeString) {
- // If there is none, use the extension to
- // identify the file, e.g. "ZIP file"
- if (fileExtension) {
- typeString =
- this.dialogElement("strings").
- getFormattedString("fileType", [fileExtension.toUpperCase()]);
- } else {
- // If we can't even do that, just give up and show the MIME type.
- typeString = this.mLauncher.MIMEInfo.MIMEType;
- }
- }
-
- var params = {};
- params.title =
- this.dialogElement("strings").getString("chooseAppFilePickerTitle");
- params.description = typeString;
- params.filename = this.mLauncher.suggestedFileName;
- params.mimeInfo = this.mLauncher.MIMEInfo;
- params.handlerApp = null;
-
- this.mDialog.openDialog("chrome://global/content/appPicker.xul", null,
- "chrome,modal,centerscreen,titlebar,dialog=yes",
- params);
-
- if (params.handlerApp &&
- params.handlerApp.executable &&
- params.handlerApp.executable.isFile()) {
- // Show the "handler" menulist since we have a (user-specified)
- // application now.
- this.dialogElement("modeDeck").setAttribute("selectedIndex", "0");
-
- // Remember the file they chose to run.
- this.chosenApp = params.handlerApp;
-
- // Update dialog
- var otherHandler = this.dialogElement("otherHandler");
- otherHandler.removeAttribute("hidden");
- otherHandler.setAttribute("path",
- this.getPath(this.chosenApp.executable));
- otherHandler.label =
- this.getFileDisplayName(this.chosenApp.executable);
- this.dialogElement("openHandler").selectedIndex = 1;
- this.dialogElement("openHandler").setAttribute("lastSelectedItemID",
- "otherHandler");
- this.dialogElement("mode").selectedItem = this.dialogElement("open");
- } else {
- var openHandler = this.dialogElement("openHandler");
- var lastSelectedID = openHandler.getAttribute("lastSelectedItemID");
- if (!lastSelectedID)
- lastSelectedID = "defaultHandler";
- openHandler.selectedItem = this.dialogElement(lastSelectedID);
- }
-
-#else
- var nsIFilePicker = Components.interfaces.nsIFilePicker;
- var fp = Components.classes["@mozilla.org/filepicker;1"]
- .createInstance(nsIFilePicker);
- fp.init(this.mDialog,
- this.dialogElement("strings").getString("chooseAppFilePickerTitle"),
- nsIFilePicker.modeOpen);
-
- fp.appendFilters(nsIFilePicker.filterApps);
-
- if (fp.show() == nsIFilePicker.returnOK && fp.file) {
- // Show the "handler" menulist since we have a (user-specified)
- // application now.
- this.dialogElement("modeDeck").setAttribute("selectedIndex", "0");
-
- // Remember the file they chose to run.
- var localHandlerApp =
- Components.classes["@mozilla.org/uriloader/local-handler-app;1"].
- createInstance(Components.interfaces.nsILocalHandlerApp);
- localHandlerApp.executable = fp.file;
- this.chosenApp = localHandlerApp;
-
- // Update dialog.
- var otherHandler = this.dialogElement("otherHandler");
- otherHandler.removeAttribute("hidden");
- otherHandler.setAttribute("path", this.getPath(this.chosenApp.executable));
-#ifdef XP_MACOSX
- this.chosenApp.executable
- .QueryInterface(Components.interfaces.nsILocalFileMac);
- otherHandler.label = this.chosenApp.executable.bundleDisplayName;
-#else
- otherHandler.label = this.chosenApp.executable.leafName;
-#endif
- this.dialogElement("openHandler").selectedIndex = 1;
- this.dialogElement("openHandler").setAttribute("lastSelectedItemID", "otherHandler");
-
- this.dialogElement("mode").selectedItem = this.dialogElement("open");
- }
- else {
- var openHandler = this.dialogElement("openHandler");
- var lastSelectedID = openHandler.getAttribute("lastSelectedItemID");
- if (!lastSelectedID)
- lastSelectedID = "defaultHandler";
- openHandler.selectedItem = this.dialogElement(lastSelectedID);
- }
-#endif
- },
-
- // Turn this on to get debugging messages.
- debug: false,
-
- // Dump text (if debug is on).
- dump: function( text ) {
- if ( this.debug ) {
- dump( text );
- }
- },
-
- // dumpInfo:
- doDebug: function() {
- const nsIProgressDialog = Components.interfaces.nsIProgressDialog;
- // Open new progress dialog.
- var progress = Components.classes[ "@mozilla.org/progressdialog;1" ]
- .createInstance( nsIProgressDialog );
- // Show it.
- progress.open( this.mDialog );
- },
-
- // dumpObj:
- dumpObj: function( spec ) {
- var val = "<undefined>";
- try {
- val = eval( "this."+spec ).toString();
- } catch( exception ) {
- }
- this.dump( spec + "=" + val + "\n" );
- },
-
- // dumpObjectProperties
- dumpObjectProperties: function( desc, obj ) {
- for( prop in obj ) {
- this.dump( desc + "." + prop + "=" );
- var val = "<undefined>";
- try {
- val = obj[ prop ];
- } catch ( exception ) {
- }
- this.dump( val + "\n" );
- }
- }
-}
-
-// This Component's module implementation. All the code below is used to get this
-// component registered and accessible via XPCOM.
-var module = {
- // registerSelf: Register this component.
- registerSelf: function (compMgr, fileSpec, location, type) {
- compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
-
- compMgr.registerFactoryLocation( this.cid,
- "Unknown Content Type Dialog",
- this.contractId,
- fileSpec,
- location,
- type );
- },
-
- // getClassObject: Return this component's factory object.
- getClassObject: function (compMgr, cid, iid) {
- if (!cid.equals(this.cid)) {
- throw Components.results.NS_ERROR_NO_INTERFACE;
- }
-
- if (!iid.equals(Components.interfaces.nsIFactory)) {
- throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
- }
-
- return this.factory;
- },
-
- /* CID for this class */
- cid: Components.ID("{F68578EB-6EC2-4169-AE19-8C6243F0ABE1}"),
-
- /* Contract ID for this class */
- contractId: "@mozilla.org/helperapplauncherdialog;1",
-
- /* factory object */
- factory: {
- // createInstance: Return a new nsProgressDialog object.
- createInstance: function (outer, iid) {
- if (outer != null)
- throw Components.results.NS_ERROR_NO_AGGREGATION;
-
- return (new nsUnknownContentTypeDialog()).QueryInterface(iid);
- }
- },
-
- // canUnload: n/a (returns true)
- canUnload: function(compMgr) {
- return true;
- }
-};
-
-// NSGetModule: Return the nsIModule object.
-function NSGetModule(compMgr, fileSpec) {
- return module;
-}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
@@ -0,0 +1,357 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Dao Gottwald <dao@mozilla.com>.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var EXPORTED_SYMBOLS = ["LightweightThemeManager"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+const MAX_USED_THEMES_COUNT = 8;
+
+const MAX_PREVIEW_SECONDS = 30;
+
+const MANDATORY = ["id", "name", "headerURL"];
+const OPTIONAL = ["footerURL", "textcolor", "accentcolor", "iconURL",
+ "previewURL", "author", "description", "homepageURL",
+ "updateURL", "version"];
+
+const PERSIST_ENABLED = true;
+const PERSIST_BYPASS_CACHE = false;
+const PERSIST_FILES = {
+ headerURL: "lightweighttheme-header",
+ footerURL: "lightweighttheme-footer"
+};
+
+__defineGetter__("_prefs", function () {
+ delete this._prefs;
+ return this._prefs =
+ Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService).getBranch("lightweightThemes.");
+});
+
+__defineGetter__("_observerService", function () {
+ delete this._observerService;
+ return this._observerService =
+ Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+});
+
+__defineGetter__("_ioService", function () {
+ delete this._ioService;
+ return this._ioService =
+ Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+});
+
+var LightweightThemeManager = {
+ get usedThemes () {
+ try {
+ return JSON.parse(_prefs.getCharPref("usedThemes"));
+ } catch (e) {
+ return [];
+ }
+ },
+
+ get currentTheme () {
+ try {
+ if (_prefs.getBoolPref("isThemeSelected"))
+ var data = this.usedThemes[0];
+ } catch (e) {}
+
+ return data || null;
+ },
+
+ get currentThemeForDisplay () {
+ var data = this.currentTheme;
+
+ if (data && PERSIST_ENABLED) {
+ for (let key in PERSIST_FILES) {
+ try {
+ if (data[key] && _prefs.getBoolPref("persisted." + key))
+ data[key] = _getLocalImageURI(PERSIST_FILES[key]).spec
+ + "?" + data.id + ";" + _version(data);
+ } catch (e) {}
+ }
+ }
+
+ return data;
+ },
+
+ set currentTheme (aData) {
+ let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ cancel.data = false;
+ _observerService.notifyObservers(cancel, "lightweight-theme-change-requested",
+ JSON.stringify(aData));
+
+ if (aData) {
+ let usedThemes = _usedThemesExceptId(aData.id);
+ if (cancel.data && _prefs.getBoolPref("isThemeSelected"))
+ usedThemes.splice(1, 0, aData);
+ else
+ usedThemes.unshift(aData);
+ _updateUsedThemes(usedThemes);
+ }
+
+ if (cancel.data)
+ return null;
+
+ if (_previewTimer) {
+ _previewTimer.cancel();
+ _previewTimer = null;
+ }
+
+ _prefs.setBoolPref("isThemeSelected", aData != null);
+ _notifyWindows(aData);
+ _observerService.notifyObservers(null, "lightweight-theme-changed", null);
+
+ if (PERSIST_ENABLED && aData)
+ _persistImages(aData);
+
+ return aData;
+ },
+
+ getUsedTheme: function (aId) {
+ var usedThemes = this.usedThemes;
+ for (let i = 0; i < usedThemes.length; i++) {
+ if (usedThemes[i].id == aId)
+ return usedThemes[i];
+ }
+ return null;
+ },
+
+ forgetUsedTheme: function (aId) {
+ var currentTheme = this.currentTheme;
+ if (currentTheme && currentTheme.id == aId)
+ this.currentTheme = null;
+
+ _updateUsedThemes(_usedThemesExceptId(aId));
+ },
+
+ previewTheme: function (aData) {
+ if (!aData)
+ return;
+
+ let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ cancel.data = false;
+ _observerService.notifyObservers(cancel, "lightweight-theme-preview-requested",
+ JSON.stringify(aData));
+ if (cancel.data)
+ return;
+
+ if (_previewTimer)
+ _previewTimer.cancel();
+ else
+ _previewTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ _previewTimer.initWithCallback(_previewTimerCallback,
+ MAX_PREVIEW_SECONDS * 1000,
+ _previewTimer.TYPE_ONE_SHOT);
+
+ _notifyWindows(aData);
+ },
+
+ resetPreview: function () {
+ if (_previewTimer) {
+ _previewTimer.cancel();
+ _previewTimer = null;
+ _notifyWindows(this.currentThemeForDisplay);
+ }
+ },
+
+ parseTheme: function (aString, aBaseURI) {
+ try {
+ var data = JSON.parse(aString);
+ } catch (e) {
+ return null;
+ }
+
+ if (!data || typeof data != "object")
+ return null;
+
+ for (let prop in data) {
+ if (typeof data[prop] == "string" &&
+ (data[prop] = data[prop].trim()) &&
+ (MANDATORY.indexOf(prop) > -1 || OPTIONAL.indexOf(prop) > -1)) {
+ if (!/URL$/.test(prop))
+ continue;
+
+ try {
+ data[prop] = _makeURI(data[prop], _makeURI(aBaseURI)).spec;
+ if (/^https:/.test(data[prop]))
+ continue;
+ if (prop != "updateURL" && /^http:/.test(data[prop]))
+ continue;
+ } catch (e) {}
+ }
+
+ delete data[prop];
+ }
+
+ for (let i = 0; i < MANDATORY.length; i++) {
+ if (!(MANDATORY[i] in data))
+ return null;
+ }
+
+ return data;
+ },
+
+ updateCurrentTheme: function () {
+ try {
+ if (!_prefs.getBoolPref("update.enabled"))
+ return;
+ } catch (e) {
+ return;
+ }
+
+ var theme = this.currentTheme;
+ if (!theme || !theme.updateURL)
+ return;
+
+ var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+
+ req.mozBackgroundRequest = true;
+ req.overrideMimeType("text/plain");
+ req.open("GET", theme.updateURL, true);
+
+ var self = this;
+ req.onload = function () {
+ if (req.status != 200)
+ return;
+
+ let newData = self.parseTheme(req.responseText, theme.updateURL);
+ if (!newData ||
+ newData.id != theme.id ||
+ _version(newData) == _version(theme))
+ return;
+
+ var currentTheme = self.currentTheme;
+ if (currentTheme && currentTheme.id == theme.id)
+ self.currentTheme = newData;
+ };
+
+ req.send(null);
+ }
+};
+
+function _usedThemesExceptId(aId)
+ LightweightThemeManager.usedThemes.filter(function (t) t.id != aId);
+
+function _version(aThemeData)
+ aThemeData.version || "";
+
+function _makeURI(aURL, aBaseURI)
+ _ioService.newURI(aURL, null, aBaseURI);
+
+function _updateUsedThemes(aList) {
+ if (aList.length > MAX_USED_THEMES_COUNT)
+ aList.length = MAX_USED_THEMES_COUNT;
+
+ _prefs.setCharPref("usedThemes", JSON.stringify(aList));
+
+ _observerService.notifyObservers(null, "lightweight-theme-list-changed", null);
+}
+
+function _notifyWindows(aThemeData) {
+ _observerService.notifyObservers(null, "lightweight-theme-styling-update",
+ JSON.stringify(aThemeData));
+}
+
+var _previewTimer;
+var _previewTimerCallback = {
+ notify: function () {
+ LightweightThemeManager.resetPreview();
+ }
+};
+
+function _persistImages(aData) {
+ function onSuccess(key) function () {
+ let current = LightweightThemeManager.currentTheme;
+ if (current && current.id == aData.id)
+ _prefs.setBoolPref("persisted." + key, true);
+ };
+
+ for (let key in PERSIST_FILES) {
+ _prefs.setBoolPref("persisted." + key, false);
+ if (aData[key])
+ _persistImage(aData[key], PERSIST_FILES[key], onSuccess(key));
+ }
+}
+
+function _getLocalImageURI(localFileName) {
+ var localFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("ProfD", Ci.nsILocalFile);
+ localFile.append(localFileName);
+ return _ioService.newFileURI(localFile);
+}
+
+function _persistImage(sourceURL, localFileName, callback) {
+ var targetURI = _getLocalImageURI(localFileName);
+ var sourceURI = _makeURI(sourceURL);
+
+ var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Ci.nsIWebBrowserPersist);
+
+ persist.persistFlags =
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION |
+ (PERSIST_BYPASS_CACHE ?
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE :
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_FROM_CACHE);
+
+ persist.progressListener = new _persistProgressListener(callback);
+
+ persist.saveURI(sourceURI, null, null, null, null, targetURI);
+}
+
+function _persistProgressListener(callback) {
+ this.onLocationChange = function () {};
+ this.onProgressChange = function () {};
+ this.onStatusChange = function () {};
+ this.onSecurityChange = function () {};
+ this.onStateChange = function (aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aRequest &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ try {
+ if (aRequest.QueryInterface(Ci.nsIHttpChannel).requestSucceeded) {
+ // success
+ callback();
+ return;
+ }
+ } catch (e) { }
+ // failure
+ }
+ };
+}
--- a/toolkit/mozapps/extensions/Makefile.in
+++ b/toolkit/mozapps/extensions/Makefile.in
@@ -29,22 +29,42 @@
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
-DEPTH = ../../..
-topsrcdir = @top_srcdir@
-srcdir = @srcdir@
-VPATH = @srcdir@
+DEPTH = ../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
include $(DEPTH)/config/autoconf.mk
-DIRS = public src
+MODULE = extensions
+
+XPIDLSRCS = \
+ nsIAddonRepository.idl \
+ nsIExtensionManager.idl \
+ $(NULL)
+
+EXTRA_COMPONENTS = nsExtensionManager.js
+GARBAGE += nsExtensionManager.js
+
+EXTRA_PP_COMPONENTS = \
+ nsAddonRepository.js \
+ nsBlocklistService.js \
+ $(NULL)
+
+EXTRA_JS_MODULES = \
+ LightweightThemeManager.jsm \
+ $(NULL)
ifdef ENABLE_TESTS
DIRS += test
endif
include $(topsrcdir)/config/rules.mk
+
+nsExtensionManager.js: nsExtensionManager.js.in
+ $(PYTHON) $(MOZILLA_DIR)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $^ > $@
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/nsAddonRepository.js
@@ -0,0 +1,383 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is the Extension Manager.
+#
+# The Initial Developer of the Original Code is mozilla.org
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Dave Townsend <dtownsend@oxymoronical.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+*/
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const PREF_GETADDONS_BROWSEADDONS = "extensions.getAddons.browseAddons";
+const PREF_GETADDONS_BROWSERECOMMENDED = "extensions.getAddons.recommended.browseURL";
+const PREF_GETADDONS_GETRECOMMENDED = "extensions.getAddons.recommended.url";
+const PREF_GETADDONS_BROWSESEARCHRESULTS = "extensions.getAddons.search.browseURL";
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+
+const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
+
+const API_VERSION = "1.2";
+
+function AddonSearchResult() {
+}
+
+AddonSearchResult.prototype = {
+ id: null,
+ name: null,
+ version: null,
+ summary: null,
+ description: null,
+ rating: null,
+ iconURL: null,
+ thumbnailURL: null,
+ homepageURL: null,
+ eula: null,
+ type: null,
+ xpiURL: null,
+ xpiHash: null,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonSearchResult])
+}
+
+function AddonRepository() {
+}
+
+AddonRepository.prototype = {
+ // The current set of results
+ _addons: null,
+
+ // Whether we are currently searching or not
+ _searching: false,
+
+ // Is this a search for recommended add-ons
+ _recommended: false,
+
+ // XHR associated with the current request
+ _request: null,
+
+ // Callback object to notify on completion
+ _callback: null,
+
+ // Maximum number of results to return
+ _maxResults: null,
+
+ get homepageURL() {
+ return Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+ .getService(Components.interfaces.nsIURLFormatter)
+ .formatURLPref(PREF_GETADDONS_BROWSEADDONS);
+ },
+
+ get isSearching() {
+ return this._searching;
+ },
+
+ getRecommendedURL: function() {
+ var urlf = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+ .getService(Components.interfaces.nsIURLFormatter);
+
+ return urlf.formatURLPref(PREF_GETADDONS_BROWSERECOMMENDED);
+ },
+
+ getSearchURL: function(aSearchTerms) {
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ var urlf = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+ .getService(Components.interfaces.nsIURLFormatter);
+
+ var url = prefs.getCharPref(PREF_GETADDONS_BROWSESEARCHRESULTS);
+ url = url.replace(/%TERMS%/g, encodeURIComponent(aSearchTerms));
+ return urlf.formatURL(url);
+ },
+
+ cancelSearch: function() {
+ this._searching = false;
+ if (this._request) {
+ this._request.abort();
+ this._request = null;
+ }
+ this._callback = null;
+ this._addons = null;
+ },
+
+ retrieveRecommendedAddons: function(aMaxResults, aCallback) {
+ if (this._searching)
+ return;
+
+ this._searching = true;
+ this._addons = [];
+ this._callback = aCallback;
+ this._recommended = true;
+ this._maxResults = aMaxResults;
+
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ var urlf = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+ .getService(Components.interfaces.nsIURLFormatter);
+
+ var uri = prefs.getCharPref(PREF_GETADDONS_GETRECOMMENDED);
+ uri = uri.replace(/%API_VERSION%/g, API_VERSION);
+ uri = urlf.formatURL(uri);
+ this._loadList(uri);
+ },
+
+ searchAddons: function(aSearchTerms, aMaxResults, aCallback) {
+ if (this._searching)
+ return;
+
+ this._searching = true;
+ this._addons = [];
+ this._callback = aCallback;
+ this._recommended = false;
+ this._maxResults = aMaxResults;
+
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ var urlf = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+ .getService(Components.interfaces.nsIURLFormatter);
+
+ var uri = prefs.getCharPref(PREF_GETADDONS_GETSEARCHRESULTS);
+ uri = uri.replace(/%API_VERSION%/g, API_VERSION);
+ // We double encode due to bug 427155
+ uri = uri.replace(/%TERMS%/g, encodeURIComponent(encodeURIComponent(aSearchTerms)));
+ uri = urlf.formatURL(uri);
+ this._loadList(uri);
+ },
+
+ // Posts results to the callback
+ _reportSuccess: function(aCount) {
+ this._searching = false;
+ this._request = null;
+ // The callback may want to trigger a new search so clear references early
+ var addons = this._addons;
+ var callback = this._callback;
+ this._callback = null;
+ this._addons = null;
+ callback.searchSucceeded(addons, addons.length, this._recommended ? -1 : aCount);
+ },
+
+ // Notifies the callback of a failure
+ _reportFailure: function(aEvent) {
+ this._searching = false;
+ this._request = null;
+ // The callback may want to trigger a new search so clear references early
+ var callback = this._callback;
+ this._callback = null;
+ this._addons = null;
+ callback.searchFailed();
+ },
+
+ // Parses an add-on entry from an <addon> element
+ _parseAddon: function(element) {
+ var em = Cc["@mozilla.org/extensions/manager;1"].
+ getService(Ci.nsIExtensionManager);
+ var app = Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULAppInfo).
+ QueryInterface(Ci.nsIXULRuntime);
+
+ var guid = element.getElementsByTagName("guid");
+ if (guid.length != 1)
+ return;
+
+ // Ignore add-ons already seen in the results
+ for (var i = 0; i < this._addons.length; i++)
+ if (this._addons[i].id == guid[0].textContent)
+ return;
+
+ // Ignore installed add-ons
+ if (em.getItemForID(guid[0].textContent) != null)
+ return;
+
+ // Ignore sandboxed add-ons
+ var status = element.getElementsByTagName("status");
+ // The status element has a unique id for each status type. 4 is Public.
+ if (status.length != 1 || status[0].getAttribute("id") != 4)
+ return;
+
+ // Ignore add-ons not compatible with this OS
+ var os = element.getElementsByTagName("compatible_os");
+ // Only the version 0 schema included compatible_os if it isn't there then
+ // we will see os compatibility on the install elements.
+ if (os.length > 0) {
+ var compatible = false;
+ var i = 0;
+ while (i < os.length && !compatible) {
+ if (os[i].textContent == "ALL" || os[i].textContent == app.OS) {
+ compatible = true;
+ break;
+ }
+ i++;
+ }
+ if (!compatible)
+ return;
+ }
+
+ // Ignore add-ons not compatible with this Application
+ compatible = false;
+ var tags = element.getElementsByTagName("compatible_applications");
+ if (tags.length != 1)
+ return;
+ var vc = Cc["@mozilla.org/xpcom/version-comparator;1"].
+ getService(Ci.nsIVersionComparator);
+ var apps = tags[0].getElementsByTagName("appID");
+ var i = 0;
+ while (i < apps.length) {
+ if (apps[i].textContent == app.ID) {
+ var minversion = apps[i].parentNode.getElementsByTagName("min_version")[0].textContent;
+ var maxversion = apps[i].parentNode.getElementsByTagName("max_version")[0].textContent;
+ if ((vc.compare(minversion, app.version) > 0) ||
+ (vc.compare(app.version, maxversion) > 0))
+ return;
+ compatible = true;
+ break;
+ }
+ i++;
+ }
+ if (!compatible)
+ return;
+
+ var addon = new AddonSearchResult();
+ addon.id = guid[0].textContent;
+ addon.rating = -1;
+ var node = element.firstChild;
+ while (node) {
+ if (node instanceof Ci.nsIDOMElement) {
+ switch (node.localName) {
+ case "name":
+ case "version":
+ case "summary":
+ case "description":
+ case "eula":
+ addon[node.localName] = node.textContent;
+ break;
+ case "rating":
+ if (node.textContent.length > 0) {
+ var rating = parseInt(node.textContent);
+ if (rating >= 0)
+ addon.rating = Math.min(5, rating);
+ }
+ break;
+ case "thumbnail":
+ addon.thumbnailURL = node.textContent;
+ break;
+ case "icon":
+ addon.iconURL = node.textContent;
+ break;
+ case "learnmore":
+ addon.homepageURL = node.textContent;
+ break;
+ case "type":
+ // The type element has an id attribute that is the id from AMO's
+ // database. This doesn't match our type values to perform a mapping
+ if (node.getAttribute("id") == 2)
+ addon.type = Ci.nsIUpdateItem.TYPE_THEME;
+ else
+ addon.type = Ci.nsIUpdateItem.TYPE_EXTENSION;
+ break;
+ case "install":
+ // No os attribute means the xpi is compatible with any os
+ if (node.hasAttribute("os")) {
+ var os = node.getAttribute("os").toLowerCase();
+ // If the os is not ALL and not the current OS then ignore this xpi
+ if (os != "all" && os != app.OS.toLowerCase())
+ break;
+ }
+ addon.xpiURL = node.textContent;
+ if (node.hasAttribute("hash"))
+ addon.xpiHash = node.getAttribute("hash");
+ break;
+ }
+ }
+ node = node.nextSibling;
+ }
+
+ // Add only if there was an xpi compatible with this os
+ if (addon.xpiURL)
+ this._addons.push(addon);
+ },
+
+ // Called when a single request has completed, parses out any add-ons and
+ // either notifies the callback or does a new request for more results
+ _listLoaded: function(aEvent) {
+ var request = aEvent.target;
+ var responseXML = request.responseXML;
+
+ if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
+ (request.status != 200 && request.status != 0)) {
+ this._reportFailure();
+ return;
+ }
+ var elements = responseXML.documentElement.getElementsByTagName("addon");
+ for (var i = 0; i < elements.length; i++) {
+ this._parseAddon(elements[i]);
+
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ if (this._addons.length == this._maxResults) {
+ this._reportSuccess(elements.length);
+ return;
+ }
+ }
+
+ if (responseXML.documentElement.hasAttribute("total_results"))
+ this._reportSuccess(responseXML.documentElement.getAttribute("total_results"));
+ else
+ this._reportSuccess(elements.length);
+ },
+
+ // Performs a new request for results
+ _loadList: function(aURI) {
+ this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsIXMLHttpRequest);
+ this._request.open("GET", aURI, true);
+ this._request.overrideMimeType("text/xml");
+
+ var self = this;
+ this._request.onerror = function(event) { self._reportFailure(event); };
+ this._request.onload = function(event) { self._listLoaded(event); };
+ this._request.send(null);
+ },
+
+ classDescription: "Addon Repository",
+ contractID: "@mozilla.org/extensions/addon-repository;1",
+ classID: Components.ID("{8eaaf524-7d6d-4f7d-ae8b-9277b324008d}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonRepository])
+}
+
+function NSGetModule(aCompMgr, aFileSpec) {
+ return XPCOMUtils.generateModule([AddonRepository]);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/nsBlocklistService.js
@@ -0,0 +1,1045 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is the Blocklist Service.
+#
+# The Initial Developer of the Original Code is
+# Mozilla Corporation.
+# Portions created by the Initial Developer are Copyright (C) 2007
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Robert Strong <robert.bugzilla@gmail.com>
+# Michael Wu <flamingice@sourmilk.net>
+# Dave Townsend <dtownsend@oxymoronical.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+*/
+
+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/FileUtils.jsm");
+
+const TOOLKIT_ID = "toolkit@mozilla.org"
+const KEY_PROFILEDIR = "ProfD";
+const KEY_APPDIR = "XCurProcD";
+const FILE_BLOCKLIST = "blocklist.xml";
+const PREF_BLOCKLIST_URL = "extensions.blocklist.url";
+const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
+const PREF_BLOCKLIST_INTERVAL = "extensions.blocklist.interval";
+const PREF_BLOCKLIST_LEVEL = "extensions.blocklist.level";
+const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
+const PREF_GENERAL_USERAGENT_LOCALE = "general.useragent.locale";
+const PREF_PARTNER_BRANCH = "app.partner.";
+const PREF_APP_DISTRIBUTION = "distribution.id";
+const PREF_APP_DISTRIBUTION_VERSION = "distribution.version";
+const PREF_APP_UPDATE_CHANNEL = "app.update.channel";
+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;
+
+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.defineLazyGetter(this, "gPref", function bls_gPref() {
+ return Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).
+ QueryInterface(Ci.nsIPrefBranch2);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gApp", function bls_gApp() {
+ return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo).
+ QueryInterface(Ci.nsIXULRuntime);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gABI", function bls_gABI() {
+ 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 = "Universal-gcc3";
+#endif
+ return abi;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gOSVersion", function bls_gOSVersion() {
+ 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 bls_gCertUtils() {
+ let temp = { };
+ Components.utils.import("resource://gre/modules/CertUtils.jsm", temp);
+ return temp;
+});
+
+function getObserverService() {
+ return Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+}
+
+/**
+ * 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.getCharPref(PREF_GENERAL_USERAGENT_LOCALE);
+ } catch (e) {}
+
+ return gPref.getCharPref(PREF_GENERAL_USERAGENT_LOCALE);
+}
+
+/**
+ * Read the update channel from defaults only. We do this to ensure that
+ * the channel is tightly coupled with the application and does not apply
+ * to other installations of the application that may use the same profile.
+ */
+function getUpdateChannel() {
+ var channel = "default";
+ var prefName;
+ var prefValue;
+
+ var defaults = gPref.getDefaultBranch(null);
+ try {
+ channel = defaults.getCharPref(PREF_APP_UPDATE_CHANNEL);
+ } catch (e) {
+ // use default when pref not found
+ }
+
+ try {
+ var partners = gPref.getChildList(PREF_PARTNER_BRANCH);
+ if (partners.length) {
+ channel += "-cck";
+ partners.sort();
+
+ for each (prefName in partners) {
+ prefValue = gPref.getCharPref(prefName);
+ channel += "-" + prefValue;
+ }
+ }
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+
+ return channel;
+}
+
+/* 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;
+}
+
+/**
+ * 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() {
+ let os = getObserverService();
+ os.addObserver(this, "xpcom-shutdown", 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);
+}
+
+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,
+ _pluginEntries: null,
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "xpcom-shutdown":
+ let os = getObserverService();
+ os.removeObserver(this, "xpcom-shutdown");
+ gPref.removeObserver("extensions.blocklist.", this);
+ break;
+ case "nsPref:changed":
+ switch (aData) {
+ 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;
+ }
+ },
+
+ /* See nsIBlocklistService */
+ isAddonBlocklisted: function(id, version, appVersion, toolkitVersion) {
+ return this.getAddonBlocklistState(id, version, appVersion, toolkitVersion) ==
+ Ci.nsIBlocklistService.STATE_BLOCKED;
+ },
+
+ /* See nsIBlocklistService */
+ getAddonBlocklistState: function(id, version, appVersion, toolkitVersion) {
+ if (!this._addonEntries)
+ this._loadBlocklist();
+ return this._getAddonBlocklistState(id, version, 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(id, version, addonEntries, appVersion, toolkitVersion) {
+ if (!gBlocklistEnabled)
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+
+ if (!appVersion)
+ appVersion = gApp.version;
+ if (!toolkitVersion)
+ toolkitVersion = gApp.platformVersion;
+
+ var blItem = addonEntries[id];
+ if (!blItem)
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+
+ for (var i = 0; i < blItem.length; ++i) {
+ if (blItem[i].includesItem(version, appVersion, toolkitVersion))
+ return blItem[i].severity >= gBlocklistLevel ? Ci.nsIBlocklistService.STATE_BLOCKED :
+ Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
+ }
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ },
+
+ 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;
+ }
+
+ dsURI = dsURI.replace(/%APP_ID%/g, gApp.ID);
+ dsURI = dsURI.replace(/%APP_VERSION%/g, gApp.version);
+ dsURI = dsURI.replace(/%PRODUCT%/g, gApp.name);
+ 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());
+ dsURI = dsURI.replace(/%CHANNEL%/g, getUpdateChannel());
+ 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(/\+/g, "%2B");
+
+ // 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;
+ }
+
+ var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsIXMLHttpRequest);
+ 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);
+
+ var self = this;
+ request.onerror = function(event) { self.onXMLError(event); };
+ request.onload = function(event) { self.onXMLLoad(event); };
+ 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._addonEntries)
+ this._loadBlocklist();
+ },
+
+ onXMLLoad: function(aEvent) {
+ var request = aEvent.target;
+ try {
+ gCertUtils.checkCert(request.channel);
+ }
+ catch (e) {
+ LOG("Blocklist::onXMLLoad: " + e);
+ return;
+ }
+ var 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 blocklistFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
+ if (blocklistFile.exists())
+ blocklistFile.remove(false);
+ var fos = FileUtils.openSafeFileOutputStream(blocklistFile);
+ fos.write(request.responseText, request.responseText.length);
+ FileUtils.closeSafeFileOutputStream(fos);
+
+ var oldAddonEntries = this._addonEntries;
+ var oldPluginEntries = this._pluginEntries;
+ this._addonEntries = { };
+ this._pluginEntries = { };
+ this._loadBlocklistFromFile(FileUtils.getFile(KEY_PROFILEDIR,
+ [FILE_BLOCKLIST]));
+
+ this._blocklistUpdated(oldAddonEntries, oldPluginEntries);
+ },
+
+ 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 = request.statusText;
+ // When status is 0 we don't have a valid channel.
+ if (status == 0)
+ statusText = "nsIXMLHttpRequest channel unavailable";
+ 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._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">
+# <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">
+# <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">
+# <versionRange>
+# <targetApplication>
+# <versionRange minVersion="1.5" maxVersion="1.5.*"/>
+# </targetApplication>
+# </versionRange>
+# <emItem id="item_5@domain"/>
+# </emItems>
+# <pluginItems>
+# <pluginItem>
+# <!-- 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>
+# </blocklist>
+ */
+
+ _loadBlocklistFromFile: function(file) {
+ if (!gBlocklistEnabled) {
+ LOG("Blocklist::_loadBlocklistFromFile: blocklist is disabled");
+ return;
+ }
+
+ if (!file.exists()) {
+ LOG("Blocklist::_loadBlocklistFromFile: XML File does not exist");
+ return;
+ }
+
+ var fileStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ fileStream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ try {
+ var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+ var doc = parser.parseFromStream(fileStream, "UTF-8", file.fileSize, "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 childNodes = doc.documentElement.childNodes;
+ this._addonEntries = this._processItemNodes(childNodes, "em",
+ this._handleEmItemNode);
+ this._pluginEntries = this._processItemNodes(childNodes, "plugin",
+ this._handlePluginItemNode);
+ }
+ catch (e) {
+ LOG("Blocklist::_loadBlocklistFromFile: Error constructing blocklist " + e);
+ return;
+ }
+ fileStream.close();
+ },
+
+ _processItemNodes: function(deChildNodes, prefix, handler) {
+ var result = [];
+ var itemNodes;
+ var containerName = prefix + "Items";
+ for (var i = 0; i < deChildNodes.length; ++i) {
+ var emItemsElement = deChildNodes.item(i);
+ if (emItemsElement instanceof Ci.nsIDOMElement &&
+ emItemsElement.localName == containerName) {
+ itemNodes = emItemsElement.childNodes;
+ break;
+ }
+ }
+ if (!itemNodes)
+ return result;
+
+ var itemName = prefix + "Item";
+ 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;
+ },
+
+ _handleEmItemNode: function(blocklistElement, result) {
+ if (!matchesOSABI(blocklistElement))
+ return;
+
+ var versionNodes = blocklistElement.childNodes;
+ var id = blocklistElement.getAttribute("id");
+ result[id] = [];
+ for (var x = 0; x < versionNodes.length; ++x) {
+ var versionRangeElement = versionNodes.item(x);
+ if (!(versionRangeElement instanceof Ci.nsIDOMElement) ||
+ versionRangeElement.localName != "versionRange")
+ continue;
+
+ result[id].push(new BlocklistItemData(versionRangeElement));
+ }
+ // if only the extension ID is specified block all versions of the
+ // extension for the current application.
+ if (result[id].length == 0)
+ result[id].push(new BlocklistItemData(null));
+ },
+
+ _handlePluginItemNode: function(blocklistElement, result) {
+ if (!matchesOSABI(blocklistElement))
+ return;
+
+ var matchNodes = blocklistElement.childNodes;
+ var blockEntry = {
+ matches: {},
+ versions: []
+ };
+ 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));
+ }
+ // 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));
+ result.push(blockEntry);
+ },
+
+ /* See nsIBlocklistService */
+ getPluginBlocklistState: function(plugin, appVersion, toolkitVersion) {
+ if (!this._pluginEntries)
+ this._loadBlocklist();
+ return this._getPluginBlocklistState(plugin, this._pluginEntries,
+ appVersion, toolkitVersion);
+ },
+
+ /**
+ * 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) {
+ if (!gBlocklistEnabled)
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+
+ if (!appVersion)
+ appVersion = gApp.version;
+ if (!toolkitVersion)
+ toolkitVersion = gApp.platformVersion;
+
+ for each (var blockEntry in 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 (var i = 0; i < blockEntry.versions.length; i++) {
+ if (blockEntry.versions[i].includesItem(plugin.version, appVersion,
+ toolkitVersion)) {
+ if (blockEntry.versions[i].severity >= gBlocklistLevel)
+ return Ci.nsIBlocklistService.STATE_BLOCKED;
+ if (blockEntry.versions[i].severity == SEVERITY_OUTDATED)
+ return Ci.nsIBlocklistService.STATE_OUTDATED;
+ return Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
+ }
+ }
+ }
+
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ },
+
+ _blocklistUpdated: function(oldAddonEntries, oldPluginEntries) {
+ var addonList = [];
+
+ var em = Cc["@mozilla.org/extensions/manager;1"].
+ getService(Ci.nsIExtensionManager);
+ var addons = em.updateAndGetNewBlocklistedItems();
+
+ for (let i = 0; i < addons.length; i++) {
+ let oldState = -1;
+ if (oldAddonEntries)
+ oldState = this._getAddonBlocklistState(addons[i].id, addons[i].version,
+ oldAddonEntries);
+ let state = this.getAddonBlocklistState(addons[i].id, addons[i].version);
+ // We don't want to re-warn about items
+ if (state == oldState)
+ continue;
+
+ addonList.push({
+ name: addons[i].name,
+ version: addons[i].version,
+ icon: addons[i].iconURL,
+ disable: false,
+ blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
+ item: addons[i]
+ });
+ }
+
+ var phs = Cc["@mozilla.org/plugin/host;1"].
+ getService(Ci.nsIPluginHost);
+ var plugins = phs.getPluginTags();
+
+ for (let i = 0; i < plugins.length; i++) {
+ let oldState = -1;
+ if (oldPluginEntries)
+ oldState = this._getPluginBlocklistState(plugins[i], oldPluginEntries);
+ let state = this.getPluginBlocklistState(plugins[i]);
+ // We don't want to re-warn about items
+ if (state == oldState)
+ continue;
+
+ if (plugins[i].blocklisted) {
+ if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
+ plugins[i].disabled = true;
+ }
+ else if (!plugins[i].disabled && state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
+ if (state == Ci.nsIBlocklistService.STATE_OUTDATED) {
+ gPref.setBoolPref(PREF_PLUGINS_NOTIFYUSER, true);
+ }
+ else {
+ addonList.push({
+ name: plugins[i].name,
+ version: plugins[i].version,
+ icon: "chrome://mozapps/skin/plugins/pluginGeneric.png",
+ disable: false,
+ blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
+ item: plugins[i]
+ });
+ }
+ }
+ plugins[i].blocklisted = state == Ci.nsIBlocklistService.STATE_BLOCKED;
+ }
+
+ if (addonList.length == 0)
+ return;
+
+ var args = {
+ restart: false,
+ list: addonList
+ };
+ // This lets the dialog get the raw js object
+ args.wrappedJSObject = args;
+
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ ww.openWindow(null, URI_BLOCKLIST_DIALOG, "",
+ "chrome,centerscreen,dialog,modal,titlebar", args);
+
+ for (let i = 0; i < addonList.length; i++) {
+ if (!addonList[i].disable)
+ continue;
+
+ if (addonList[i].item instanceof Ci.nsIUpdateItem)
+ em.disableItem(addonList[i].item.id);
+ else if (addonList[i].item instanceof Ci.nsIPluginTag)
+ addonList[i].item.disabled = true;
+ else
+ LOG("Blocklist::_blocklistUpdated: Unknown add-on type: " +
+ addonList[i].item);
+ }
+
+ if (args.restart)
+ restartApp();
+ },
+
+ classDescription: "Blocklist Service",
+ contractID: "@mozilla.org/extensions/blocklist;1",
+ classID: Components.ID("{66354bc9-7ed1-4692-ae1d-8da97d6b205e}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIBlocklistService,
+ Ci.nsITimerCallback]),
+ _xpcom_categories: [{ category: "update-timer",
+ value: "@mozilla.org/extensions/blocklist;1," +
+ "getService,blocklist-background-update-timer," +
+ PREF_BLOCKLIST_INTERVAL + ",86400" }]
+};
+
+/**
+ * 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;
+ 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 (var x = 0; x < blTargetApp.length; ++x) {
+ if (this.matchesRange(appVersion, blTargetApp[x].minVersion, blTargetApp[x].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 };
+ }
+};
+
+function NSGetModule(aCompMgr, aFileSpec) {
+ return XPCOMUtils.generateModule([Blocklist]);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/nsExtensionManager.js.in
@@ -0,0 +1,8361 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is the Extension Manager.
+#
+# The Initial Developer of the Original Code is Ben Goodger.
+# Portions created by the Initial Developer are Copyright (C) 2004
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Ben Goodger <ben@mozilla.org> (Google Inc.)
+# Benjamin Smedberg <benjamin@smedbergs.us>
+# Jens Bannmann <jens.b@web.de>
+# Robert Strong <robert.bugzilla@gmail.com>
+# Dave Townsend <dtownsend@oxymoronical.com>
+# Daniel Veditz <dveditz@mozilla.com>
+# Alexander J. Vincent <ajvincent@gmail.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+*/
+
+//
+// TODO:
+// - better logging
+//
+
+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/LightweightThemeManager.jsm");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+
+const PREF_EM_CHECK_COMPATIBILITY = "extensions.checkCompatibility";
+const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion";
+const PREF_EM_ENABLED_ITEMS = "extensions.enabledItems";
+const PREF_UPDATE_COUNT = "extensions.update.count";
+const PREF_UPDATE_DEFAULT_URL = "extensions.update.url";
+const PREF_EM_NEW_ADDONS_LIST = "extensions.newAddons";
+const PREF_EM_DISABLED_ADDONS_LIST = "extensions.disabledAddons";
+const PREF_EM_SHOW_MISMATCH_UI = "extensions.showMismatchUI";
+const PREF_EM_IGNOREMTIMECHANGES = "extensions.ignoreMTimeChanges";
+const PREF_EM_DISABLEDOBSOLETE = "extensions.disabledObsolete";
+const PREF_EM_EXTENSION_FORMAT = "extensions.%UUID%.";
+const PREF_EM_ITEM_UPDATE_ENABLED = "extensions.%UUID%.update.enabled";
+const PREF_EM_UPDATE_ENABLED = "extensions.update.enabled";
+const PREF_EM_ITEM_UPDATE_URL = "extensions.%UUID%.update.url";
+const PREF_EM_DSS_ENABLED = "extensions.dss.enabled";
+const PREF_DSS_SWITCHPENDING = "extensions.dss.switchPending";
+const PREF_DSS_SKIN_TO_SELECT = "extensions.lastSelectedSkin";
+const PREF_LWTHEME_TO_SELECT = "extensions.lwThemeToSelect";
+const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
+const PREF_EM_LOGGING_ENABLED = "extensions.logging.enabled";
+const PREF_EM_UPDATE_INTERVAL = "extensions.update.interval";
+const PREF_UPDATE_NOTIFYUSER = "extensions.update.notifyUser";
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+
+const DIR_EXTENSIONS = "extensions";
+const DIR_CHROME = "chrome";
+const DIR_STAGE = "staged-xpis";
+const FILE_EXTENSIONS = "extensions.rdf";
+const FILE_EXTENSION_MANIFEST = "extensions.ini";
+const FILE_EXTENSIONS_STARTUP_CACHE = "extensions.cache";
+const FILE_EXTENSIONS_LOG = "extensions.log";
+const FILE_INSTALL_MANIFEST = "install.rdf";
+const FILE_CHROME_MANIFEST = "chrome.manifest";
+
+const UNKNOWN_XPCOM_ABI = "unknownABI";
+
+const TOOLKIT_ID = "toolkit@mozilla.org"
+
+const KEY_PROFILEDIR = "ProfD";
+const KEY_PROFILEDS = "ProfDS";
+const KEY_APPDIR = "XCurProcD";
+const KEY_TEMPDIR = "TmpD";
+
+const EM_ACTION_REQUESTED_TOPIC = "em-action-requested";
+const EM_ITEM_INSTALLED = "item-installed";
+const EM_ITEM_UPGRADED = "item-upgraded";
+const EM_ITEM_UNINSTALLED = "item-uninstalled";
+const EM_ITEM_ENABLED = "item-enabled";
+const EM_ITEM_DISABLED = "item-disabled";
+const EM_ITEM_CANCEL = "item-cancel-action";
+
+const OP_NONE = "";
+const OP_NEEDS_INSTALL = "needs-install";
+const OP_NEEDS_UPGRADE = "needs-upgrade";
+const OP_NEEDS_UNINSTALL = "needs-uninstall";
+const OP_NEEDS_ENABLE = "needs-enable";
+const OP_NEEDS_DISABLE = "needs-disable";
+
+const KEY_APP_PROFILE = "app-profile";
+const KEY_APP_GLOBAL = "app-global";
+const KEY_APP_SYSTEM_LOCAL = "app-system-local";
+const KEY_APP_SYSTEM_SHARE = "app-system-share";
+const KEY_APP_SYSTEM_USER = "app-system-user";
+
+const CATEGORY_INSTALL_LOCATIONS = "extension-install-locations";
+const CATEGORY_UPDATE_PARAMS = "extension-update-params";
+
+const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
+const PREFIX_ITEM_URI = "urn:mozilla:item:";
+const PREFIX_EXTENSION = "urn:mozilla:extension:";
+const PREFIX_THEME = "urn:mozilla:theme:";
+const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest";
+const RDFURI_ITEM_ROOT = "urn:mozilla:item:root"
+const RDFURI_DEFAULT_THEME = "urn:mozilla:item:{972ce4c6-7e08-4474-a285-3208198ce6fd}";
+const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
+
+const URI_GENERIC_ICON_XPINSTALL = "chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png";
+const URI_GENERIC_ICON_THEME = "chrome://mozapps/skin/extensions/themeGeneric.png";
+const URI_XPINSTALL_CONFIRM_DIALOG = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul";
+const URI_EXTENSIONS_PROPERTIES = "chrome://mozapps/locale/extensions/extensions.properties";
+const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties";
+const URI_DOWNLOADS_PROPERTIES = "chrome://mozapps/locale/downloads/downloads.properties";
+const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
+const URI_EXTENSION_LIST_DIALOG = "chrome://mozapps/content/extensions/list.xul";
+
+const URI_EXTENSION_MANAGER = "chrome://mozapps/content/extensions/extensions.xul";
+const FEATURES_EXTENSION_MANAGER = "chrome,menubar,extra-chrome,toolbar,dialog=no,resizable";
+const FEATURES_EXTENSION_UPDATES = "chrome,centerscreen,extra-chrome,dialog,resizable,modal";
+
+/**
+ * Constants that internal code can use to indicate the reason for an add-on
+ * update check. external code uses other constants in nsIExtensionManager.idl.
+ */
+const MAX_PUBLIC_UPDATE_WHEN = 15;
+const UPDATE_WHEN_PERIODIC_UPDATE = 16;
+const UPDATE_WHEN_ADDON_INSTALLED = 17;
+
+/**
+ * Bitmask of the different types of update check.
+ */
+const UPDATE_TYPE_COMPATIBILITY = 32;
+const UPDATE_TYPE_NEWVERSION = 64;
+
+const INSTALLERROR_SUCCESS = 0;
+const INSTALLERROR_INVALID_VERSION = -1;
+const INSTALLERROR_INVALID_GUID = -2;
+const INSTALLERROR_INCOMPATIBLE_VERSION = -3;
+const INSTALLERROR_PHONING_HOME = -4;
+const INSTALLERROR_INCOMPATIBLE_PLATFORM = -5;
+const INSTALLERROR_BLOCKLISTED = -6;
+const INSTALLERROR_INSECURE_UPDATE = -7;
+const INSTALLERROR_INVALID_MANIFEST = -8;
+const INSTALLERROR_RESTRICTED = -9;
+const INSTALLERROR_SOFTBLOCKED = -10;
+
+const REQ_VERSION = 2;
+
+var gApp = null;
+var gPref = null;
+var gRDF = null;
+var gOS = null;
+var gCheckCompatibilityPref;
+var gEmSingleton = null;
+var gBlocklist = null;
+var gXPCOMABI = null;
+var gOSTarget = null;
+var gConsole = null;
+var gInstallManifestRoot = null;
+var gVersionChecker = null;
+var gLoggingEnabled = null;
+var gCheckCompatibility = true;
+var gCheckUpdateSecurity = true;
+var gLocale = "en-US";
+var gFirstRun = false;
+var gAllowFlush = true;
+var gDSNeedsFlush = false;
+var gManifestNeedsFlush = false;
+var gDefaultTheme = "classic/1.0";
+
+/**
+ * Valid GUIDs fit this pattern.
+ */
+var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
+
+var gBranchVersion = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
+
+// 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;
+});
+
+/**
+ * Creates a Version Checker object.
+ * @returns A handle to the global Version Checker service.
+ */
+function getVersionChecker() {
+ if (!gVersionChecker) {
+ gVersionChecker = Cc["@mozilla.org/xpcom/version-comparator;1"].
+ getService(Ci.nsIVersionComparator);
+ }
+ return gVersionChecker;
+}
+
+var BundleManager = {
+ /**
+ * Creates and returns a String Bundle at the specified URI
+ * @param bundleURI
+ * The URI of the bundle to load
+ * @returns A nsIStringBundle which was retrieved.
+ */
+ getBundle: function BundleManager_getBundle(bundleURI) {
+ var sbs = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ return sbs.createBundle(bundleURI);
+ },
+
+ _appName: "",
+
+ /**
+ * The Application's display name.
+ */
+ get appName() {
+ if (!this._appName) {
+ var brandBundle = this.getBundle(URI_BRAND_PROPERTIES)
+ this._appName = brandBundle.GetStringFromName("brandShortName");
+ }
+ return this._appName;
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// Utility Functions
+//
+function EM_NS(property) {
+ return PREFIX_NS_EM + property;
+}
+
+function EM_R(property) {
+ return gRDF.GetResource(EM_NS(property));
+}
+
+function EM_L(literal) {
+ return gRDF.GetLiteral(literal);
+}
+
+function EM_I(integer) {
+ return gRDF.GetIntLiteral(integer);
+}
+
+function EM_D(integer) {
+ return gRDF.GetDateLiteral(integer);
+}
+
+/**
+ * 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;
+}
+
+/**
+ * Initializes a RDF Container at a URI in a datasource.
+ * @param datasource
+ * The datasource the container is in
+ * @param root
+ * The RDF Resource which is the root of the container.
+ * @returns The nsIRDFContainer, initialized at the root.
+ */
+function getContainer(datasource, root) {
+ var ctr = Cc["@mozilla.org/rdf/container;1"].
+ createInstance(Ci.nsIRDFContainer);
+ ctr.Init(datasource, root);
+ return ctr;
+}
+
+/**
+ * Gets a RDF Resource for item with the given ID
+ * @param id
+ * The GUID of the item to construct a RDF resource to the
+ * active item for
+ * @returns The RDF Resource to the Active item.
+ */
+function getResourceForID(id) {
+ return gRDF.GetResource(PREFIX_ITEM_URI + id);
+}
+
+/**
+ * Construct a nsIUpdateItem with the supplied metadata
+ * ...
+ */
+function makeItem(id, version, locationKey, minVersion, maxVersion, name,
+ updateURL, updateHash, iconURL, updateRDF, updateKey, type,
+ targetAppID) {
+ var item = new UpdateItem();
+ item.init(id, version, locationKey, minVersion, maxVersion, name,
+ updateURL, updateHash, iconURL, updateRDF, updateKey, type,
+ targetAppID);
+ return item;
+}
+
+/**
+ * Gets the descriptor of a directory as a relative path to common base
+ * directories (profile, user home, app install dir, etc).
+ *
+ * @param itemLocation
+ * The nsILocalFile representing the item's directory.
+ * @param installLocation the nsIInstallLocation for this item
+ */
+function getDescriptorFromFile(itemLocation, installLocation) {
+ var baseDir = installLocation.location;
+
+ if (baseDir && baseDir.contains(itemLocation, true)) {
+ return "rel%" + itemLocation.getRelativeDescriptor(baseDir);
+ }
+
+ return "abs%" + itemLocation.persistentDescriptor;
+}
+
+function getAbsoluteDescriptor(itemLocation) {
+ return itemLocation.persistentDescriptor;
+}
+
+/**
+ * Initializes a Local File object based on a descriptor
+ * provided by "getDescriptorFromFile".
+ *
+ * @param descriptor
+ * The descriptor that locates the directory
+ * @param installLocation
+ * The nsIInstallLocation object for this item.
+ * @returns The nsILocalFile object representing the location of the item
+ */
+function getFileFromDescriptor(descriptor, installLocation) {
+ var location = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+
+ var m = descriptor.match(/^(abs|rel)\%(.*)$/);
+ if (!m)
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ if (m[1] == "rel") {
+ location.setRelativeDescriptor(installLocation.location, m[2]);
+ }
+ else {
+ location.persistentDescriptor = m[2];
+ }
+
+ return location;
+}
+
+/**
+ * Determines if a file is an item package - either a XPI or a JAR file.
+ * @param file
+ * The file to check
+ * @returns true if the file is an item package, false otherwise.
+ */
+function fileIsItemPackage(file) {
+ var fileURL = getURIFromFile(file);
+ if (fileURL instanceof Ci.nsIURL)
+ var extension = fileURL.fileExtension.toLowerCase();
+ return extension == "xpi" || extension == "jar";
+}
+
+/**
+ * Deletes a directory and its children. First it tries nsIFile::Remove(true).
+ * If that fails it will fall back to recursing, setting the appropriate
+ * permissions, and deleting the current entry. This is needed for when we have
+ * rights to delete a directory but there are entries that have a read-only
+ * attribute (e.g. a copy restore from a read-only CD, etc.)
+ * @param dir
+ * A nsIFile for the directory to be deleted
+ */
+function removeDirRecursive(dir) {
+ try {
+ dir.remove(true);
+ return;
+ }
+ catch (e) {
+ }
+
+ var dirEntries = dir.directoryEntries;
+ while (dirEntries.hasMoreElements()) {
+ var entry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
+
+ if (entry.isDirectory()) {
+ removeDirRecursive(entry);
+ }
+ else {
+ entry.permissions = FileUtils.PERMS_FILE;
+ entry.remove(false);
+ }
+ }
+ dir.permissions = FileUtils.PERMS_DIRECTORY;
+ dir.remove(true);
+}
+
+/**
+ * Logs a string to the error console and the text console if logging is
+ * enabled.
+ * @param string
+ * The log message.
+ */
+function LOG(string) {
+ if (gLoggingEnabled) {
+ dump("*** EM_LOG *** " + string + "\n");
+ if (gConsole)
+ gConsole.logStringMessage(string);
+ }
+}
+
+/**
+ * Logs a warning to the error console and if logging is enabled to the text
+ * console.
+ * @param string
+ * The warning message.
+ */
+function WARN(string) {
+ if (gLoggingEnabled)
+ dump("*** EM_WARN *** " + string + "\n");
+ if (gConsole) {
+ var message = Cc["@mozilla.org/scripterror;1"].
+ createInstance(Ci.nsIScriptError);
+ message.init(string, null, null, 0, 0, Ci.nsIScriptError.warningFlag,
+ "component javascript");
+ gConsole.logMessage(message);
+ }
+}
+
+/**
+ * Logs an error to the error console and to a permanent log file.
+ * @param string
+ * The error message.
+ */
+function ERROR(string) {
+ if (gLoggingEnabled)
+ dump("*** EM_ERROR *** " + string + "\n");
+ if (gConsole) {
+ var message = Cc["@mozilla.org/scripterror;1"].
+ createInstance(Ci.nsIScriptError);
+ message.init(string, null, null, 0, 0, Ci.nsIScriptError.errorFlag,
+ "component javascript");
+ gConsole.logMessage(message);
+ }
+ try {
+ var tstamp = new Date();
+ var logfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS_LOG]);
+ var stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ stream.init(logfile, 0x02 | 0x08 | 0x10, 0666, 0); // write, create, append
+ var writer = Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(Ci.nsIConverterOutputStream);
+ writer.init(stream, "UTF-8", 0, 0x0000);
+ string = tstamp.toLocaleFormat("%Y-%m-%d %H:%M:%S - ") + string;
+ writer.writeString(string + "\n");
+ writer.close();
+ }
+ catch (e) { }
+}
+
+/**
+ * Randomize the specified file name. Used to force RDF to bypass the cache
+ * when loading certain types of files.
+ * @param fileName
+ * A file name to randomize, e.g. install.rdf
+ * @returns A randomized file name, e.g. install-xyz.rdf
+ */
+function getRandomFileName(fileName) {
+ var extensionDelimiter = fileName.lastIndexOf(".");
+ var prefix = fileName.substr(0, extensionDelimiter);
+ var suffix = fileName.substr(extensionDelimiter);
+
+ var characters = "abcdefghijklmnopqrstuvwxyz0123456789";
+ var nameString = prefix + "-";
+ for (var i = 0; i < 3; ++i) {
+ var index = Math.round((Math.random()) * characters.length);
+ nameString += characters.charAt(index);
+ }
+ return nameString + "." + suffix;
+}
+
+/**
+ * Get the RDF URI prefix of a nsIUpdateItem type. This function should be used
+ * ONLY to support Firefox 1.0 Update RDF files! Item URIs in the datasource
+ * are NOT prefixed.
+ * @param type
+ * The nsIUpdateItem type to find a RDF URI prefix for
+ * @returns The RDF URI prefix.
+ */
+function getItemPrefix(type) {
+ if (type & Ci.nsIUpdateItem.TYPE_EXTENSION)
+ return PREFIX_EXTENSION;
+ else if (type & Ci.nsIUpdateItem.TYPE_THEME)
+ return PREFIX_THEME;
+ return PREFIX_ITEM_URI;
+}
+
+/**
+ * Trims a prefix from a string.
+ * @param string
+ * The source string
+ * @param prefix
+ * The prefix to remove.
+ * @returns The suffix (string - prefix)
+ */
+function stripPrefix(string, prefix) {
+ return string.substr(prefix.length);
+}
+
+/**
+ * Gets a File URL spec for a nsIFile
+ * @param file
+ * The file to get a file URL spec to
+ * @returns The file URL spec to the file
+ */
+function getURLSpecFromFile(file) {
+ var ioServ = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ var fph = ioServ.getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ return fph.getURLSpecFromFile(file);
+}
+
+/**
+ * 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);
+}
+
+/**
+ * Constructs a File URI to a nsIFile
+ * @param file
+ * The file to construct a File URI to
+ * @returns The file URI to the file
+ */
+function getURIFromFile(file) {
+ var ioServ = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ return ioServ.newFileURI(file);
+}
+
+/**
+ * @returns Whether or not we are currently running in safe mode.
+ */
+function inSafeMode() {
+ return gApp.inSafeMode;
+}
+
+/**
+ * Extract the string value from a RDF Literal or Resource
+ * @param literalOrResource
+ * RDF String Literal or Resource
+ * @returns String value of the literal or resource, or undefined if the object
+ * supplied is not a RDF string literal or resource.
+ */
+function stringData(literalOrResource) {
+ if (literalOrResource instanceof Ci.nsIRDFLiteral)
+ return literalOrResource.Value;
+ if (literalOrResource instanceof Ci.nsIRDFResource)
+ return literalOrResource.Value;
+ return undefined;
+}
+
+/**
+ * Extract the integer value of a RDF Literal
+ * @param literal
+ * nsIRDFInt literal
+ * @return integer value of the literal
+ */
+function intData(literal) {
+ if (literal instanceof Ci.nsIRDFInt)
+ return literal.Value;
+ return undefined;
+}
+
+/**
+ * Gets a property from an install manifest.
+ * @param installManifest
+ * An Install Manifest datasource to read from
+ * @param property
+ * The name of a proprety to read (sans EM_NS)
+ * @returns The literal value of the property, or undefined if the property has
+ * no value.
+ */
+function getManifestProperty(installManifest, property) {
+ var target = installManifest.GetTarget(gInstallManifestRoot,
+ gRDF.GetResource(EM_NS(property)), true);
+ var val = stringData(target);
+ return val === undefined ? intData(target) : val;
+}
+
+/**
+ * Given an Install Manifest Datasource, retrieves the type of item the manifest
+ * describes.
+ * @param installManifest
+ * The Install Manifest Datasource.
+ * @return The nsIUpdateItem type of the item described by the manifest
+ * returns TYPE_EXTENSION if attempts to determine the type fail.
+ */
+function getAddonTypeFromInstallManifest(installManifest) {
+ var target = installManifest.GetTarget(gInstallManifestRoot,
+ gRDF.GetResource(EM_NS("type")), true);
+ if (target) {
+ var type = stringData(target);
+ return type === undefined ? intData(target) : parseInt(type);
+ }
+
+ // Firefox 1.0 and earlier did not support addon-type annotation on the
+ // Install Manifest, so we fall back to a theme-only property to
+ // differentiate.
+ if (getManifestProperty(installManifest, "internalName") !== undefined)
+ return Ci.nsIUpdateItem.TYPE_THEME;
+
+ // If no type is provided, default to "Extension"
+ return Ci.nsIUpdateItem.TYPE_EXTENSION;
+}
+
+/**
+ * Shows a message about an incompatible Extension/Theme.
+ * @param installData
+ * An Install Data object from |getInstallData|
+ */
+function showIncompatibleError(installData) {
+ var extensionStrings = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
+ var params = [extensionStrings.GetStringFromName("type-" + installData.type)];
+ var title = extensionStrings.formatStringFromName("incompatibleTitle",
+ params, params.length);
+ params = [installData.name, installData.version, BundleManager.appName,
+ gApp.version];
+ var message = extensionStrings.formatStringFromName("incompatibleMessage",
+ params, params.length);
+ var ps = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService);
+ ps.alert(null, title, message);
+}
+
+/**
+ * Shows a message.
+ * @param titleKey
+ * String key of the title string in the Extensions localization file.
+ * @param messageKey
+ * String key of the message string in the Extensions localization file.
+ * @param messageParams
+ * Array of strings to be substituted into |messageKey|. Can be null.
+ */
+function showMessage(titleKey, titleParams, messageKey, messageParams) {
+ var extensionStrings = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
+ if (titleParams && titleParams.length > 0) {
+ var title = extensionStrings.formatStringFromName(titleKey, titleParams,
+ titleParams.length);
+ }
+ else
+ title = extensionStrings.GetStringFromName(titleKey);
+
+ if (messageParams && messageParams.length > 0) {
+ var message = extensionStrings.formatStringFromName(messageKey, messageParams,
+ messageParams.length);
+ }
+ else
+ message = extensionStrings.GetStringFromName(messageKey);
+ var ps = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService);
+ ps.alert(null, title, message);
+}
+
+/**
+ * Shows a dialog for a blocklisted item. For soft blocked items this will
+ * return true if the item should still be installed
+ * @param item
+ * The nsIUpdateItem that is blocklisted
+ * @param softblocked
+ * True if this item is only soft blocked and may still be installed.
+ */
+function showBlocklistMessage(item, softblocked) {
+ var params = Cc["@mozilla.org/embedcomp/dialogparam;1"].
+ createInstance(Ci.nsIDialogParamBlock);
+ params.SetInt(0, softblocked ? 1 : 0);
+ params.SetInt(1, 0);
+ params.SetNumberStrings(1);
+ params.SetString(0, item.name + " " + item.version);
+
+ var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ var win = wm.getMostRecentWindow("Extension:Manager");
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ ww.openWindow(win, URI_EXTENSION_LIST_DIALOG, "",
+ "chrome,centerscreen,modal,dialog,titlebar", params);
+
+ return params.GetInt(1) == 0 ? false : true;
+}
+
+/**
+ * Gets a zip reader for the file specified.
+ * @param zipFile
+ * A ZIP archive to open with a nsIZipReader.
+ * @return A nsIZipReader for the file specified.
+ */
+function getZipReaderForFile(zipFile) {
+ try {
+ var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ zipReader.open(zipFile);
+ }
+ catch (e) {
+ zipReader.close();
+ throw e;
+ }
+ return zipReader;
+}
+
+/**
+ * Verifies that a zip file's contents are all signed by the same principal.
+ * Directory entries and anything in the META-INF directory are not checked.
+ * @param zip
+ * A nsIZipReader to check
+ * @param principal
+ * The nsIPrincipal to compare against
+ * @return true if all the contents were signed by the principal, false
+ * otherwise.
+ */
+function verifyZipSigning(zip, principal) {
+ var count = 0;
+ var entries = zip.findEntries(null);
+ while (entries.hasMore()) {
+ var entry = entries.getNext();
+ // Nothing in META-INF is in the manifest.
+ if (entry.substr(0, 9) == "META-INF/")
+ continue;
+ // Directory entries aren't in the manifest.
+ if (entry.substr(-1) == "/")
+ continue;
+ count++;
+ var entryPrincipal = zip.getCertificatePrincipal(entry);
+ if (!entryPrincipal || !principal.equals(entryPrincipal))
+ return false;
+ }
+ return zip.manifestEntriesCount == count;
+}
+
+/**
+ * Extract a RDF file from a ZIP archive to a random location in the system
+ * temp directory.
+ * @param zipFile
+ * A ZIP archive to read from
+ * @param fileName
+ * The name of the file to read from the zip.
+ * @param suppressErrors
+ * Whether or not to report errors.
+ * @return The file created in the temp directory.
+ */
+function extractRDFFileToTempDir(zipFile, fileName, suppressErrors) {
+ var file = FileUtils.getFile(KEY_TEMPDIR, [getRandomFileName(fileName)]);
+ try {
+ var zipReader = getZipReaderForFile(zipFile);
+ zipReader.extract(fileName, file);
+ zipReader.close();
+ }
+ catch (e) {
+ if (!suppressErrors) {
+ showMessage("missingFileTitle", [], "missingFileMessage",
+ [BundleManager.appName, fileName]);
+ throw e;
+ }
+ }
+ return file;
+}
+
+/**
+ * Gets an Install Manifest datasource from a file.
+ * @param file
+ * The nsIFile that contains the Install Manifest RDF
+ * @returns The Install Manifest datasource
+ */
+function getInstallManifest(file) {
+ var uri = getURIFromFile(file);
+ try {
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(file, -1, -1, false);
+ var bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
+ createInstance(Ci.nsIBufferedInputStream);
+ bis.init(fis, 4096);
+
+ var rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
+ createInstance(Ci.nsIRDFXMLParser)
+ var ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
+ createInstance(Ci.nsIRDFDataSource);
+ var listener = rdfParser.parseAsync(ds, uri);
+ var channel = Cc["@mozilla.org/network/input-stream-channel;1"].
+ createInstance(Ci.nsIInputStreamChannel);
+ channel.setURI(uri);
+ channel.contentStream = bis;
+ channel.QueryInterface(Ci.nsIChannel);
+ channel.contentType = "text/xml";
+
+ listener.onStartRequest(channel, null);
+ try {
+ var pos = 0;
+ var count = bis.available();
+ while (count > 0) {
+ listener.onDataAvailable(channel, null, bis, pos, count);
+ pos += count;
+ count = bis.available();
+ }
+ listener.onStopRequest(channel, null, Components.results.NS_OK);
+ bis.close();
+ fis.close();
+
+ var arcs = ds.ArcLabelsOut(gInstallManifestRoot);
+ if (arcs.hasMoreElements())
+ return ds;
+ }
+ catch (e) {
+ listener.onStopRequest(channel, null, e.result);
+ bis.close();
+ fis.close();
+ }
+ }
+ catch (e) { }
+
+ var url = uri.QueryInterface(Ci.nsIURL);
+ showMessage("malformedTitle", [], "malformedMessage",
+ [BundleManager.appName, url.fileName]);
+ return null;
+}
+
+/**
+ * Selects the closest matching localized resource in the given RDF resource
+ * @param aDataSource The datasource to look in
+ * @param aResource The root resource containing the localized sections
+ * @returns The nsIRDFResource of the best em:localized section or null
+ * if no valid match was found
+ */
+function findClosestLocalizedResource(aDataSource, aResource) {
+ var localizedProp = EM_R("localized");
+ var localeProp = EM_R("locale");
+
+ // Holds the best matching localized resource
+ var bestmatch = null;
+ // The number of locale parts it matched with
+ var bestmatchcount = 0;
+ // The number of locale parts in the match
+ var bestpartcount = 0;
+
+ var locales = [gLocale.toLowerCase()];
+ /* If the current locale is English then it will find a match if there is
+ a valid match for en-US so no point searching that locale too. */
+ if (locales[0].substring(0, 3) != "en-")
+ locales.push("en-us");
+
+ for each (var locale in locales) {
+ var lparts = locale.split("-");
+ var localizations = aDataSource.GetTargets(aResource, localizedProp, true);
+ while (localizations.hasMoreElements()) {
+ var localized = localizations.getNext().QueryInterface(Ci.nsIRDFNode);
+ var list = aDataSource.GetTargets(localized, localeProp, true);
+ while (list.hasMoreElements()) {
+ var found = stringData(list.getNext().QueryInterface(Ci.nsIRDFNode));
+ if (!found)
+ continue;
+
+ found = found.toLowerCase();
+
+ // Exact match is returned immediately
+ if (locale == found)
+ return localized;
+
+ var fparts = found.split("-");
+ /* If we have found a possible match and this one isn't any longer
+ then we dont need to check further. */
+ if (bestmatch && fparts.length < bestmatchcount)
+ continue;
+
+ // Count the number of parts that match
+ var maxmatchcount = Math.min(fparts.length, lparts.length);
+ var matchcount = 0;
+ while (matchcount < maxmatchcount &&
+ fparts[matchcount] == lparts[matchcount])
+ matchcount++;
+
+ /* If we matched more than the last best match or matched the same and
+ this locale is less specific than the last best match. */
+ if (matchcount > bestmatchcount ||
+ (matchcount == bestmatchcount && fparts.length < bestpartcount)) {
+ bestmatch = localized;
+ bestmatchcount = matchcount;
+ bestpartcount = fparts.length;
+ }
+ }
+ }
+ // If we found a valid match for this locale return it
+ if (bestmatch)
+ return bestmatch;
+ }
+ return null;
+}
+
+/**
+ * An enumeration of items in a JS array.
+ * @constructor
+ */
+function ArrayEnumerator(aItems) {
+ if (aItems) {
+ for (var i = 0; i < aItems.length; ++i) {
+ if (!aItems[i])
+ aItems.splice(i--, 1);
+ }
+ this._contents = aItems;
+ } else {
+ this._contents = [];
+ }
+}
+
+ArrayEnumerator.prototype = {
+ _index: 0,
+
+ hasMoreElements: function ArrayEnumerator_hasMoreElements() {
+ return this._index < this._contents.length;
+ },
+
+ getNext: function ArrayEnumerator_getNext() {
+ return this._contents[this._index++];
+ }
+};
+
+/**
+ * An enumeration of files in a JS array.
+ * @param files
+ * The files to enumerate
+ * @constructor
+ */
+function FileEnumerator(files) {
+ if (files) {
+ for (var i = 0; i < files.length; ++i) {
+ if (!files[i])
+ files.splice(i--, 1);
+ }
+ this._contents = files;
+ } else {
+ this._contents = [];
+ }
+}
+
+FileEnumerator.prototype = {
+ _index: 0,
+
+ /**
+ * Gets the next file in the sequence.
+ */
+ get nextFile() {
+ if (this._index < this._contents.length)
+ return this._contents[this._index++];
+ return null;
+ },
+
+ /**
+ * Stop enumerating. Nothing to do here.
+ */
+ close: function FileEnumerator_close() {
+ }
+};
+
+/**
+ * An object which identifies an Install Location for items, where the location
+ * relationship is each item living in a directory named with its GUID under
+ * the directory used when constructing this object.
+ *
+ * e.g. <location>\{GUID1}
+ * <location>\{GUID2}
+ * <location>\{GUID3}
+ * ...
+ *
+ * @param name
+ * The string identifier of this Install Location.
+ * @param location
+ * The directory that contains the items.
+ * @constructor
+ */
+function DirectoryInstallLocation(name, location, restricted, priority, independent) {
+ this._name = name;
+ if (location.exists()) {
+ if (!location.isDirectory())
+ throw new Error("location must be a directoy!");
+ }
+ else {
+ try {
+ location.create(Ci.nsILocalFile.DIRECTORY_TYPE, 0775);
+ }
+ catch (e) {
+ LOG("DirectoryInstallLocation: failed to create location " +
+ " directory = " + location.path + ", exception = " + e + "\n");
+ }
+ }
+
+ this._location = location;
+ this._locationToIDMap = {};
+ this._restricted = restricted;
+ this._priority = priority;
+ this._independent = independent;
+}
+DirectoryInstallLocation.prototype = {
+ _name : "",
+ _location : null,
+ _locationToIDMap: null,
+ _restricted : false,
+ _priority : 0,
+ _independent : false,
+ _canAccess : null,
+
+ /**
+ * See nsIExtensionManager.idl
+ */
+ get name() {
+ return this._name;
+ },
+
+ /**
+ * Reads a directory linked to in a file.
+ * @param file
+ * The file containing the directory path
+ * @returns A nsILocalFile object representing the linked directory.
+ */
+ _readDirectoryFromFile: function DirInstallLocation__readDirectoryFromFile(file) {
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(file, -1, -1, false);
+ var line = { value: "" };
+ if (fis instanceof Ci.nsILineInputStream)
+ fis.readLine(line);
+ fis.close();
+ if (line.value) {
+ var linkedDirectory = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ try {
+ linkedDirectory.initWithPath(line.value);
+ }
+ catch (e) {
+ linkedDirectory.setRelativeDescriptor(file.parent, line.value);
+ }
+
+ return linkedDirectory;
+ }
+ return null;
+ },
+
+ /**
+ * See nsIExtensionManager.idl
+ */
+ get itemLocations() {
+ var locations = [];
+ if (!this._location.exists())
+ return new FileEnumerator(locations);
+
+ try {
+ var entries = this._location.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ while (true) {
+ var entry = entries.nextFile;
+ if (!entry)
+ break;
+ entry instanceof Ci.nsILocalFile;
+ if (!entry.isDirectory() && gIDTest.test(entry.leafName)) {
+ var linkedDirectory = this._readDirectoryFromFile(entry);
+ if (linkedDirectory && linkedDirectory.exists() &&
+ linkedDirectory.isDirectory()) {
+ locations.push(linkedDirectory);
+ this._locationToIDMap[linkedDirectory.persistentDescriptor] = entry.leafName;
+ }
+ }
+ else
+ locations.push(entry);
+ }
+ entries.close();
+ }
+ catch (e) {
+ }
+ return new FileEnumerator(locations);
+ },
+
+ /**
+ * Retrieves the GUID for an item at the specified location.
+ * @param file
+ * The location where an item might live.
+ * @returns The ID for an item that might live at the location specified.
+ *
+ * N.B. This function makes no promises about whether or not this path is
+ * actually maintained by this Install Location.
+ */
+ getIDForLocation: function DirInstallLocation_getIDForLocation(file) {
+ var section = file.leafName;
+ var filePD = file.persistentDescriptor;
+ if (filePD in this._locationToIDMap)
+ section = this._locationToIDMap[filePD];
+
+ if (gIDTest.test(section))
+ return RegExp.$1;
+ return undefined;
+ },
+
+ /**
+ * See nsIExtensionManager.idl
+ */
+ get location() {
+ return this._location.clone();
+ },
+
+ /**
+ * See nsIExtensionManager.idl
+ */
+ get restricted() {
+ return this._restricted;
+ },
+
+ /**
+ * See nsIExtensionManager.idl
+ */
+ get canAccess() {
+ if (this._canAccess != null)
+ return this._canAccess;
+
+ if (!this.location.exists()) {
+ this._canAccess = false;
+ return false;
+ }
+
+ var testFile = this.location;
+ testFile.append("Access Privileges Test");
+ try {
+ testFile.createUnique(Ci.nsILocalFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY);
+ testFile.remove(false);
+ this._canAccess = true;
+ }
+ catch (e) {
+ this._canAccess = false;
+ }
+ return this._canAccess;
+ },
+
+ /**
+ * See nsIExtensionManager.idl
+ */
+ get priority() {
+ return this._priority;
+ },
+
+ /**
+ * See nsIExtensionManager.idl
+ */
+ getItemLocation: function DirInstallLocation_getItemLocation(id) {
+ var itemLocation = this.location;
+ itemLocation.append(id);
+ if (itemLocation.exists() && !itemLocation.isDirectory())
+ return this._readDirectoryFromFile(itemLocation);
+ if (!itemLocation.exists() && this.canAccess)
+ itemLocation.create(Ci.nsILocalFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY);
+ return itemLocation;
+ },
+
+ /**
+ * See nsIExtensionManager.idl
+ */
+ itemIsManagedIndependently: function DirInstallLocation_itemIsManagedIndependently(id) {
+ if (this._independent)
+ return true;
+ var itemLocation = this.location;
+ itemLocation.append(id);
+ return itemLocation.exists() && !itemLocation.isDirectory();
+ },
+
+ /**
+ * See nsIExtensionManager.idl
+ */
+ getItemFile: function DirInstallLocation_getItemFile(id, filePath) {
+ var itemLocation = this.getItemLocation(id);
+ var parts = filePath.split("/");
+ for (var i = 0; i < parts.length; ++i)
+ itemLocation.append(parts[i]);
+ return itemLocation;
+ },
+
+ /**
+ * Stages the specified file for later.
+ * @param file
+ * The file to stage
+ * @param id
+ * The GUID of the item the file represents
+ */
+ stageFile: function DirInstallLocation_stageFile(file, id) {
+ var stagedFile = this.location;
+ stagedFile.append(DIR_STAGE);
+ stagedFile.append(id);
+ stagedFile.append(file.leafName);
+
+ // When an incompatible update is successful the file is already staged
+ if (stagedFile.equals(file))
+ return stagedFile;
+
+ if (stagedFile.exists())
+ stagedFile.remove(false);
+
+ file.copyTo(stagedFile.parent, stagedFile.leafName);
+
+ // If the file has incorrect permissions set, correct them now.
+ if (!stagedFile.isWritable())
+ stagedFile.permissions = FileUtils.PERMS_FILE;
+
+ return stagedFile;
+ },
+
+ /**
+ * Returns the most recently staged package (e.g. the last XPI or JAR in a
+ * directory) for an item and removes items that do not qualify.
+ * @param id
+ * The ID of the staged package
+ * @returns an nsIFile if the package exists otherwise null.
+ */
+ getStageFile: function DirInstallLocation_getStageFile(id) {
+ var stageFile = null;
+ var stageDir = this.location;
+ stageDir.append(DIR_STAGE);
+ stageDir.append(id);
+ if (!stageDir.exists() || !stageDir.isDirectory())
+ return null;
+ try {
+ var entries = stageDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ while (entries.hasMoreElements()) {
+ var file = entries.nextFile;
+ if (!(file instanceof Ci.nsILocalFile))
+ continue;
+ if (file.isDirectory())
+ removeDirRecursive(file);
+ else if (fileIsItemPackage(file)) {
+ if (stageFile)
+ stageFile.remove(false);
+ stageFile = file;
+ }
+ else
+ file.remove(false);
+ }
+ }
+ catch (e) {
+ }
+ if (entries instanceof Ci.nsIDirectoryEnumerator)
+ entries.close();
+ return stageFile;
+ },
+
+ /**
+ * Removes a file from the stage. This cleans up the stage if there is nothing
+ * else left after the remove operation.
+ * @param file
+ * The file to remove.
+ */
+ removeFile: function DirInstallLocation_removeFile(file) {
+ if (file.exists())
+ file.remove(false);
+ var parent = file.parent;
+ var entries = parent.directoryEntries;
+ try {
+ // XXXrstrong calling hasMoreElements on a nsIDirectoryEnumerator after
+ // it has been removed will cause a crash on Mac OS X - bug 292823
+ while (parent && !parent.equals(this.location) &&
+ !entries.hasMoreElements()) {
+ parent.remove(false);
+ parent = parent.parent;
+ entries = parent.directoryEntries;
+ }
+ if (entries instanceof Ci.nsIDirectoryEnumerator)
+ entries.close();
+ }
+ catch (e) {
+ ERROR("DirectoryInstallLocation::removeFile: failed to remove staged " +
+ " directory = " + parent.path + ", exception = " + e + "\n");
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIInstallLocation])
+};
+
+#ifdef XP_WIN
+
+const nsIWindowsRegKey = Ci.nsIWindowsRegKey;
+
+/**
+ * An object that identifies the location of installed items based on entries
+ * in the Windows registry. For each application a subkey is defined that
+ * contains a set of values, where the name of each value is a GUID and the
+ * contents of the value is a filesystem path identifying a directory
+ * containing an installed item.
+ *
+ * @param name
+ * The string identifier of this Install Location.
+ * @param rootKey
+ * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
+ * @param restricted
+ * Indicates that the location may be restricted (e.g., this is
+ * usually true of a system level install location).
+ * @param priority
+ * The priority of this install location.
+ * @constructor
+ */
+function WinRegInstallLocation(name, rootKey, restricted, priority) {
+ this._name = name;
+ this._rootKey = rootKey;
+ this._restricted = restricted;
+ this._priority = priority;
+ this._IDToDirMap = {};
+ this._DirToIDMap = {};
+
+ // Reading the registry may throw an exception, and that's ok. In error
+ // cases, we just leave ourselves in the empty state.
+ try {
+ var path = this._appKeyPath + "\\Extensions";
+ var key = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(nsIWindowsRegKey);
+ key.open(this._rootKey, path, nsIWindowsRegKey.ACCESS_READ);
+ this._readAddons(key);
+ } catch (e) {
+ if (key)
+ key.close();
+ }
+}
+WinRegInstallLocation.prototype = {
+ _name : "",
+ _rootKey : null,
+ _restricted : false,
+ _priority : 0,
+ _IDToDirMap : null, // mapping from ID to directory object
+ _DirToIDMap : null, // mapping from directory path to ID
+
+ /**
+ * Retrieves the path of this Application's data key in the registry.
+ */
+ get _appKeyPath() {
+ var appVendor = gApp.vendor;
+ var appName = gApp.name;
+
+#ifdef MOZ_THUNDERBIRD
+ // XXX Thunderbird doesn't specify a vendor string!!
+ if (appVendor == "")
+ appVendor = "Mozilla";
+#endif
+
+ // XULRunner-based apps may intentionally not specify a vendor:
+ if (appVendor != "")
+ appVendor += "\\";
+
+ return "SOFTWARE\\" + appVendor + appName;
+ },
+
+ /**
+ * Read the registry and build a mapping between GUID and directory for each
+ * installed item.
+ * @param key
+ * The key that contains the GUID->path pairs
+ */
+ _readAddons: function RegInstallLocation__readAddons(key) {
+ var count = key.valueCount;
+ for (var i = 0; i < count; ++i) {
+ var id = key.getValueName(i);
+
+ var dir = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ dir.initWithPath(key.readStringValue(id));
+
+ if (dir.exists() && dir.isDirectory()) {
+ this._IDToDirMap[id] = dir;
+ this._DirToIDMap[dir.path] = id;
+ }
+ }
+ },
+
+ get name() {
+ return this._name;
+ },
+
+ get itemLocations() {
+ var locations = [];
+ for (var id in this._IDToDirMap) {
+ locations.push(this._IDToDirMap[id]);
+ }
+ return new FileEnumerator(locations);
+ },
+
+ get location() {
+ return null;
+ },
+
+ get restricted() {
+ return this._restricted;
+ },
+
+ // you should never be able to write to this location
+ get canAccess() {
+ return false;
+ },
+
+ get priority() {
+ return this._priority;
+ },
+
+ getItemLocation: function RegInstallLocation_getItemLocation(id) {
+ if (!(id in this._IDToDirMap))
+ return null;
+ return this._IDToDirMap[id].clone();
+ },
+
+ getIDForLocation: function RegInstallLocation_getIDForLocation(dir) {
+ return this._DirToIDMap[dir.path];
+ },
+
+ getItemFile: function RegInstallLocation_getItemFile(id, filePath) {
+ var itemLocation = this.getItemLocation(id);
+ if (!itemLocation)
+ return null;
+ var parts = filePath.split("/");
+ for (var i = 0; i < parts.length; ++i)
+ itemLocation.append(parts[i]);
+ return itemLocation;
+ },
+
+ itemIsManagedIndependently: function RegInstallLocation_itemIsManagedIndependently(id) {
+ return true;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIInstallLocation])
+};
+
+#endif
+
+/**
+ * Safely attempt to install or uninstall a given item ID in an install
+ * location. Using aggressive success-safety checks, this function will attempt
+ * to move an existing location for an item aside and then allow installation
+ * into the appropriate folder. If any operation fails the installation will
+ * abort and roll back from the moved-aside old version.
+ * @param itemID
+ * The GUID of the item to perform the operation on.
+ * @param installLocation
+ * The Install Location where the item is installed.
+ * @param file
+ * An xpi file to install to the location or null to just uninstall
+ */
+function safeInstallOperation(itemID, installLocation, file) {
+ var movedFiles = [];
+
+ /**
+ * Reverts a deep move by moving backed up files back to their original
+ * location.
+ */
+ function rollbackMove()
+ {
+ for (var i = 0; i < movedFiles.length; ++i) {
+ var oldFile = movedFiles[i].oldFile;
+ var newFile = movedFiles[i].newFile;
+ try {
+ newFile.moveTo(oldFile.parent, newFile.leafName);
+ }
+ catch (e) {
+ ERROR("safeInstallOperation: failed to roll back files after an install " +
+ "operation failed. Failed to roll back: " + newFile.path + " to: " +
+ oldFile.path + " ... aborting installation.");
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Moves a file to a new folder.
+ * @param file
+ * The file to move
+ * @param destination
+ * The target folder
+ */
+ function moveFile(file, destination) {
+ try {
+ var oldFile = file.clone();
+ file.moveTo(destination, file.leafName);
+ movedFiles.push({ oldFile: oldFile, newFile: file });
+ }
+ catch (e) {
+ ERROR("safeInstallOperation: failed to back up file: " + file.path + " to: " +
+ destination.path + " ... rolling back file moves and aborting " +
+ "installation.");
+ rollbackMove();
+ throw e;
+ }
+ }
+
+ /**
+ * Moves a directory to a new location. If any part of the move fails,
+ * files already moved will be rolled back.
+ * @param sourceDir
+ * The directory to move
+ * @param targetDir
+ * The destination directory
+ * @param currentDir
+ * The current directory (a subdirectory of |sourceDir| or
+ * |sourceDir| itself) we are moving files from.
+ */
+ function moveDirectory(sourceDir, targetDir, currentDir) {
+ var entries = currentDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ while (true) {
+ var entry = entries.nextFile;
+ if (!entry)
+ break;
+ if (entry.isDirectory())
+ moveDirectory(sourceDir, targetDir, entry);
+ else if (entry instanceof Ci.nsILocalFile) {
+ var rd = entry.getRelativeDescriptor(sourceDir);
+ var destination = targetDir.clone().QueryInterface(Ci.nsILocalFile);
+ destination.setRelativeDescriptor(targetDir, rd);
+ moveFile(entry, destination.parent);
+ }
+ }
+ entries.close();
+ }
+
+ /**
+ * Removes the temporary backup directory where we stored files.
+ * @param directory
+ * The backup directory to remove
+ */
+ function cleanUpTrash(directory) {
+ try {
+ // Us-generated. Safe.
+ if (directory && directory.exists())
+ removeDirRecursive(directory);
+ }
+ catch (e) {
+ ERROR("safeInstallOperation: failed to clean up the temporary backup of the " +
+ "older version: " + itemLocationTrash.path);
+ // This is a non-fatal error. Annoying, but non-fatal.
+ }
+ }
+
+ if (!installLocation.itemIsManagedIndependently(itemID)) {
+ var itemLocation = installLocation.getItemLocation(itemID);
+ if (itemLocation.exists()) {
+ var trashDirName = itemID + "-trash";
+ var itemLocationTrash = itemLocation.parent.clone();
+ itemLocationTrash.append(trashDirName);
+ if (itemLocationTrash.exists()) {
+ // We can remove recursively here since this is a folder we created, not
+ // one the user specified. If this fails, it'll throw, and the caller
+ // should stop installation.
+ try {
+ removeDirRecursive(itemLocationTrash);
+ }
+ catch (e) {
+ ERROR("safeFileOperation: failed to remove existing trash directory " +
+ itemLocationTrash.path + " ... aborting installation.");
+ throw e;
+ }
+ }
+
+ // Move the directory that contains the existing version of the item aside,
+ // into {GUID}-trash. This will throw if there's a failure and the install
+ // will abort.
+ moveDirectory(itemLocation, itemLocationTrash, itemLocation);
+
+ // Clean up the original location, if necessary. Again, this is a path we
+ // generated, so it is safe to recursively delete.
+ try {
+ removeDirRecursive(itemLocation);
+ }
+ catch (e) {
+ ERROR("safeInstallOperation: failed to clean up item location after its contents " +
+ "were properly backed up. Failed to clean up: " + itemLocation.path +
+ " ... rolling back file moves and aborting installation.");
+ rollbackMove();
+ cleanUpTrash(itemLocationTrash);
+ throw e;
+ }
+ }
+ }
+ else if (installLocation.name == KEY_APP_PROFILE ||
+ installLocation.name == KEY_APP_GLOBAL ||
+ installLocation.name == KEY_APP_SYSTEM_USER) {
+ // Check for a pointer file and move it aside if it exists
+ var pointerFile = installLocation.location.clone();
+ pointerFile.append(itemID);
+ if (pointerFile.exists() && !pointerFile.isDirectory()) {
+ var trashFileName = itemID + "-trash";
+ var itemLocationTrash = installLocation.location.clone();
+ itemLocationTrash.append(trashFileName);
+ if (itemLocationTrash.exists()) {
+ // We can remove recursively here since this is a folder we created, not
+ // one the user specified. If this fails, it'll throw, and the caller
+ // should stop installation.
+ try {
+ removeDirRecursive(itemLocationTrash);
+ }
+ catch (e) {
+ ERROR("safeFileOperation: failed to remove existing trash directory " +
+ itemLocationTrash.path + " ... aborting installation.");
+ throw e;
+ }
+ }
+ itemLocationTrash.create(Ci.nsILocalFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY);
+ // Move the pointer file to the trash.
+ moveFile(pointerFile, itemLocationTrash);
+ }
+ }
+
+ if (file) {
+ // Extract the xpi's files into the new directory
+ try {
+ var zipReader = getZipReaderForFile(file);
+
+ // create directories first
+ var entries = zipReader.findEntries("*/");
+ while (entries.hasMore()) {
+ var entryName = entries.getNext();
+ var target = installLocation.getItemFile(itemID, entryName);
+ if (!target.exists()) {
+ try {
+ target.create(Ci.nsILocalFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY);
+ }
+ catch (e) {
+ ERROR("extractFiles: failed to create target directory for extraction " +
+ "file = " + target.path + ", exception = " + e + "\n");
+ }
+ }
+ }
+
+ entries = zipReader.findEntries(null);
+ while (entries.hasMore()) {
+ var entryName = entries.getNext();
+ target = installLocation.getItemFile(itemID, entryName);
+ if (target.exists())
+ continue;
+
+ zipReader.extract(entryName, target);
+ target.permissions = FileUtils.PERMS_FILE;
+ }
+ }
+ catch (e) {
+ // This means the install operation failed. Remove everything and roll back.
+ ERROR("safeInstallOperation: file extraction failed, " +
+ "rolling back file moves and aborting installation.");
+ try {
+ // Us-generated. Safe.
+ removeDirRecursive(itemLocation);
+ }
+ catch (e) {
+ ERROR("safeInstallOperation: failed to remove the folder we failed to install " +
+ "an item into: " + itemLocation.path + " -- There is not much to suggest " +
+ "here... maybe restart and try again?");
+ cleanUpTrash(itemLocationTrash);
+ throw e;
+ }
+ rollbackMove();
+ cleanUpTrash(itemLocationTrash);
+ throw e;
+ }
+ finally {
+ if (zipReader)
+ zipReader.close();
+ }
+ }
+
+ // Now, and only now - after everything else has succeeded (against all odds!)
+ // remove the {GUID}-trash directory where we stashed the old version of the
+ // item.
+ cleanUpTrash(itemLocationTrash);
+}
+
+/**
+ * Manages the list of pending operations.
+ */
+var PendingOperations = {
+ _ops: { },
+
+ /**
+ * Adds an entry to the Pending Operations List
+ * @param opType
+ * The type of Operation to be performed
+ * @param entry
+ * A JS Object representing the item to be operated on:
+ * "locationKey" The name of the Install Location where the item
+ * is installed.
+ * "id" The GUID of the item.
+ */
+ addItem: function PendingOperations_addItem(opType, entry) {
+ if (opType == OP_NONE)
+ this.clearOpsForItem(entry.id);
+ else {
+ if (!(opType in this._ops))
+ this._ops[opType] = { };
+ this._ops[opType][entry.id] = entry.locationKey;
+ }
+ },
+
+ /**
+ * Removes a Pending Operation from the list
+ * @param opType
+ * The type of Operation being removed
+ * @param id
+ * The GUID of the item to remove the entry for
+ */
+ clearItem: function PendingOperations_clearItem(opType, id) {
+ if (opType in this._ops && id in this._ops[opType])
+ delete this._ops[opType][id];
+ },
+
+ /**
+ * Removes all Pending Operation for an item
+ * @param id
+ * The ID of the item to remove the entries for
+ */
+ clearOpsForItem: function PendingOperations_clearOpsForItem(id) {
+ for (var opType in this._ops) {
+ if (id in this._ops[opType])
+ delete this._ops[opType][id];
+ }
+ },
+
+ /**
+ * Remove all Pending Operations of a certain type
+ * @param opType
+ * The type of Operation to remove all entries for
+ */
+ clearItems: function PendingOperations_clearItems(opType) {
+ if (opType in this._ops)
+ delete this._ops[opType];
+ },
+
+ /**
+ * Get an array of operations of a certain type
+ * @param opType
+ * The type of Operation to return a list of
+ */
+ getOperations: function PendingOperations_getOperations(opType) {
+ if (!(opType in this._ops))
+ return [];
+ var ops = [];
+ for (var id in this._ops[opType])
+ ops.push( {id: id, locationKey: this._ops[opType][id] } );
+ return ops;
+ },
+
+ /**
+ * The total number of Pending Operations, for all types.
+ */
+ get size() {
+ var size = 0;
+ for (var opType in this._ops) {
+ for (var id in this._ops[opType])
+ ++size;
+ }
+ return size;
+ }
+};
+
+/**
+ * Manages registered Install Locations
+ */
+var InstallLocations = {
+ _locations: { },
+
+ /**
+ * A nsISimpleEnumerator of all available Install Locations.
+ */
+ get enumeration() {
+ var installLocations = [];
+ for (var key in this._locations)
+ installLocations.push(InstallLocations.get(key));
+ return new ArrayEnumerator(installLocations);
+ },
+
+ /**
+ * Gets a named Install Location
+ * @param name
+ * The name of the Install Location to get
+ */
+ get: function InstallLocations_get(name) {
+ return name in this._locations ? this._locations[name] : null;
+ },
+
+ /**
+ * Registers an Install Location
+ * @param installLocation
+ * The Install Location to register
+ */
+ put: function InstallLocations_put(installLocation) {
+ this._locations[installLocation.name] = installLocation;
+ }
+};
+
+/**
+ * Manages the Startup Cache. The Startup Cache is a representation
+ * of the contents of extensions.cache, a list of all
+ * items the Extension System knows about, whether or not they
+ * are active or visible.
+ */
+var StartupCache = {
+ /**
+ * Location Name -> GUID hash of entries from the Startup Cache file
+ * Each entry has the following properties:
+ * "descriptor" The location on disk of the item
+ * "mtime" The time the location was last modified
+ * "op" Any pending operations on this item.
+ * "location" The Install Location name where the item is installed.
+ */
+ entries: { },
+
+ /**
+ * Puts an entry into the Startup Cache
+ * @param installLocation
+ * The Install Location where the item is installed
+ * @param id
+ * The GUID of the item
+ * @param op
+ * The name of the operation to be performed
+ * @param shouldCreate
+ * Whether or not we should create a new entry for this item
+ * in the cache if one does not already exist.
+ */
+ put: function StartupCache_put(installLocation, id, op, shouldCreate) {
+ var itemLocation = installLocation.getItemLocation(id);
+
+ var descriptor = null;
+ var mtime = null;
+ if (itemLocation) {
+ itemLocation.QueryInterface(Ci.nsILocalFile);
+ descriptor = getDescriptorFromFile(itemLocation, installLocation);
+ if (itemLocation.exists() && itemLocation.isDirectory())
+ mtime = Math.floor(itemLocation.lastModifiedTime / 1000);
+ }
+
+ this._putRaw(installLocation.name, id, descriptor, mtime, op, shouldCreate);
+ },
+
+ /**
+ * Private helper function for putting an entry into the Startup Cache
+ * without relying on the presence of its associated nsIInstallLocation
+ * instance.
+ *
+ * @param key
+ * The install location name.
+ * @param id
+ * The ID of the item.
+ * @param descriptor
+ * Value returned from absoluteDescriptor. May be null, in which
+ * case the descriptor field is not updated.
+ * @param mtime
+ * The last modified time of the item. May be null, in which case the
+ * descriptor field is not updated.
+ * @param op
+ * The OP code to store with the entry.
+ * @param shouldCreate
+ * Boolean value indicating whether to create or delete the entry.
+ */
+ _putRaw: function StartupCache__putRaw(key, id, descriptor, mtime, op, shouldCreate) {
+ if (!(key in this.entries))
+ this.entries[key] = { };
+ if (!(id in this.entries[key]))
+ this.entries[key][id] = { };
+ if (shouldCreate) {
+ if (!this.entries[key][id])
+ this.entries[key][id] = { };
+
+ var entry = this.entries[key][id];
+
+ if (descriptor)
+ entry.descriptor = descriptor;
+ if (mtime)
+ entry.mtime = mtime;
+ entry.op = op;
+ entry.location = key;
+ }
+ else
+ this.entries[key][id] = null;
+ },
+
+ /**
+ * Clears an entry from the Startup Cache
+ * @param installLocation
+ * The Install Location where item is installed
+ * @param id
+ * The GUID of the item.
+ */
+ clearEntry: function StartupCache_clearEntry(installLocation, id) {
+ var key = installLocation.name;
+ if (key in this.entries && id in this.entries[key])
+ this.entries[key][id] = null;
+ },
+
+ /**
+ * Get all the startup cache entries for a particular ID.
+ * @param id
+ * The GUID of the item to locate.
+ * @returns An array of Startup Cache entries for the specified ID.
+ */
+ findEntries: function StartupCache_findEntries(id) {
+ var entries = [];
+ for (var key in this.entries) {
+ if (id in this.entries[key])
+ entries.push(this.entries[key][id]);
+ }
+ return entries;
+ },
+
+ /**
+ * Read the Item-Change manifest file into a hash of properties.
+ * The Item-Change manifest currently holds a list of paths, with the last
+ * mtime for each path, and the GUID of the item at that path.
+ */
+ read: function StartupCache_read() {
+ var itemChangeManifest = FileUtils.getFile(KEY_PROFILEDIR,
+ [FILE_EXTENSIONS_STARTUP_CACHE]);
+ if (!itemChangeManifest.exists()) {
+ // There is no change manifest for some reason, either we're in an initial
+ // state or something went wrong with one of the other files and the
+ // change manifest was removed. Return an empty dataset and rebuild.
+ gFirstRun = true;
+ return;
+ }
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(itemChangeManifest, -1, -1, false);
+ if (fis instanceof Ci.nsILineInputStream) {
+ var line = { value: "" };
+ var more = false;
+ do {
+ more = fis.readLine(line);
+ if (line.value) {
+ // The Item-Change manifest is formatted like so:
+ // (pd = descriptor)
+ // location-key\tguid-of-item\tpd-to-extension1\tmtime-of-pd\tpending-op
+ // location-key\tguid-of-item\tpd-to-extension2\tmtime-of-pd\tpending-op
+ // ...
+ // We hash on location-key first, because we don't want to have to
+ // spin up the main extensions datasource on every start to determine
+ // the Install Location for an item.
+ // We hash on guid second, because we want a way to quickly determine
+ // item GUID during a check loop that runs on every startup.
+ var parts = line.value.split("\t");
+ // Silently drop any entries in unknown install locations
+ if (!InstallLocations.get(parts[0]))
+ continue;
+ var op = parts[4];
+ this._putRaw(parts[0], parts[1], parts[2], parts[3], op, true);
+ if (op)
+ PendingOperations.addItem(op, { locationKey: parts[0], id: parts[1] });
+ }
+ }
+ while (more);
+ }
+ fis.close();
+ },
+
+ /**
+ * Writes the Startup Cache to disk
+ */
+ write: function StartupCache_write() {
+ var extensionsCacheFile = FileUtils.getFile(KEY_PROFILEDIR,
+ [FILE_EXTENSIONS_STARTUP_CACHE]);
+ var fos = FileUtils.openSafeFileOutputStream(extensionsCacheFile);
+ for (var locationKey in this.entries) {
+ for (var id in this.entries[locationKey]) {
+ var entry = this.entries[locationKey][id];
+ if (entry) {
+ try {
+ var itemLocation = getFileFromDescriptor(entry.descriptor, InstallLocations.get(locationKey));
+
+ // Update our knowledge of this item's last-modified-time.
+ // XXXdarin: this may cause us to miss changes in some cases.
+ var itemMTime = 0;
+ if (itemLocation.exists() && itemLocation.isDirectory())
+ itemMTime = Math.floor(itemLocation.lastModifiedTime / 1000);
+
+ // Each line in the startup cache manifest is in this form:
+ // location-key\tid-of-item\tpd-to-extension1\tmtime-of-pd\tpending-op
+ var line = locationKey + "\t" + id + "\t" + entry.descriptor + "\t" +
+ itemMTime + "\t" + entry.op + "\r\n";
+ fos.write(line, line.length);
+ }
+ catch (e) {}
+ }
+ }
+ }
+ FileUtils.closeSafeFileOutputStream(fos);
+ }
+};
+
+/**
+ * Installs, manages and tracks compatibility for Extensions and Themes
+ * @constructor
+ */
+function ExtensionManager() {
+ gApp = Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULAppInfo).QueryInterface(Ci.nsIXULRuntime);
+ gOSTarget = gApp.OS;
+ try {
+ gXPCOMABI = gApp.XPCOMABI;
+ } catch (ex) {
+ // Provide a default for gXPCOMABI. It won't be compared to an
+ // item's metadata (i.e. install.rdf can't specify e.g. WINNT_unknownABI
+ // as targetPlatform), but it will be displayed in error messages and
+ // transmitted to update URLs.
+ gXPCOMABI = UNKNOWN_XPCOM_ABI;
+ }
+ gPref = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch2).
+ QueryInterface(Ci.nsIPrefService);
+ var defaults = gPref.getDefaultBranch("");
+ try {
+ gDefaultTheme = defaults.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN);
+ } catch(e) {}
+
+ gOS = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ gOS.addObserver(this, "xpcom-shutdown", false);
+ gOS.addObserver(this, "lightweight-theme-preview-requested", false);
+ gOS.addObserver(this, "lightweight-theme-change-requested", false);
+
+ gConsole = Cc["@mozilla.org/consoleservice;1"].
+ getService(Ci.nsIConsoleService);
+
+ gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].
+ getService(Ci.nsIRDFService);
+ gInstallManifestRoot = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT);
+
+ // Register Global Install Location
+ var appGlobalExtensions = FileUtils.getDir(KEY_APPDIR, [DIR_EXTENSIONS],
+ false);
+ var priority = Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_GLOBAL;
+ var globalLocation = new DirectoryInstallLocation(KEY_APP_GLOBAL,
+ appGlobalExtensions, true,
+ priority, false);
+ InstallLocations.put(globalLocation);
+
+ // Register App-Profile Install Location
+ var appProfileExtensions = FileUtils.getDir(KEY_PROFILEDS, [DIR_EXTENSIONS],
+ false);
+ var priority = Ci.nsIInstallLocation.PRIORITY_APP_PROFILE;
+ var profileLocation = new DirectoryInstallLocation(KEY_APP_PROFILE,
+ appProfileExtensions, false,
+ priority, false);
+ InstallLocations.put(profileLocation);
+
+ // Register per-user Install Location
+ try {
+ var appSystemUExtensions = FileUtils.getDir("XREUSysExt", [gApp.ID], false);
+ }
+ catch(e) { }
+
+ if (appSystemUExtensions) {
+ var priority = Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_USER;
+ var systemLocation = new DirectoryInstallLocation(KEY_APP_SYSTEM_USER,
+ appSystemUExtensions, false,
+ priority, true);
+
+ InstallLocations.put(systemLocation);
+ }
+
+ // Register App-System-Shared Install Location
+ try {
+ var appSystemSExtensions = FileUtils.getDir("XRESysSExtPD", [gApp.ID], false);
+ }
+ catch (e) { }
+
+ if (appSystemSExtensions) {
+ var priority = Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_GLOBAL + 10;
+ var systemLocation = new DirectoryInstallLocation(KEY_APP_SYSTEM_SHARE,
+ appSystemSExtensions, true,
+ priority, true);
+ InstallLocations.put(systemLocation);
+ }
+
+ // Register App-System-Local Install Location
+ try {
+ var appSystemLExtensions = FileUtils.getDir("XRESysLExtPD", [gApp.ID], false);
+ }
+ catch (e) { }
+
+ if (appSystemLExtensions) {
+ var priority = Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_GLOBAL + 20;
+ var systemLocation = new DirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL,
+ appSystemLExtensions, true,
+ priority, true);
+ InstallLocations.put(systemLocation);
+ }
+
+#ifdef XP_WIN
+ // Register HKEY_LOCAL_MACHINE Install Location
+ InstallLocations.put(
+ new WinRegInstallLocation("winreg-app-global",
+ nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ true,
+ Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_GLOBAL + 10));
+
+ // Register HKEY_CURRENT_USER Install Location
+ InstallLocations.put(
+ new WinRegInstallLocation("winreg-app-user",
+ nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ false,
+ Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_USER + 10));
+#endif
+
+ // Register Additional Install Locations
+ var categoryManager = Cc["@mozilla.org/categorymanager;1"].
+ getService(Ci.nsICategoryManager);
+ var locations = categoryManager.enumerateCategory(CATEGORY_INSTALL_LOCATIONS);
+ while (locations.hasMoreElements()) {
+ var entry = locations.getNext().QueryInterface(Ci.nsISupportsCString).data;
+ var contractID = categoryManager.getCategoryEntry(CATEGORY_INSTALL_LOCATIONS, entry);
+ var location = Cc[contractID].getService(Ci.nsIInstallLocation);
+ InstallLocations.put(location);
+ }
+}
+
+ExtensionManager.prototype = {
+ /**
+ * See nsIObserver.idl
+ */
+ observe: function EM_observe(subject, topic, data) {
+ switch (topic) {
+ case "profile-after-change":
+ this._profileSelected();
+ break;
+ case "quit-application-requested":
+ this._confirmCancelDownloadsOnQuit(subject);
+ break;
+ case "offline-requested":
+ this._confirmCancelDownloadsOnOffline(subject);
+ break;
+ case "lightweight-theme-preview-requested":
+ if (gPref.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN)) {
+ let cancel = subject.QueryInterface(Ci.nsISupportsPRBool);
+ cancel.data = true;
+ }
+ break;
+ case "lightweight-theme-change-requested":
+ let theme = JSON.parse(data);
+ if (!theme)
+ return;
+
+ if (gPref.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN)) {
+ if (getPref("getBoolPref", PREF_EM_DSS_ENABLED, false)) {
+ gPref.clearUserPref(PREF_GENERAL_SKINS_SELECTEDSKIN);
+ return;
+ }
+
+ let cancel = subject.QueryInterface(Ci.nsISupportsPRBool);
+ cancel.data = true;
+ gPref.setBoolPref(PREF_DSS_SWITCHPENDING, true);
+ gPref.setCharPref(PREF_DSS_SKIN_TO_SELECT, gDefaultTheme);
+ gPref.setCharPref(PREF_LWTHEME_TO_SELECT, theme.id);
+
+ // Open the UI so the user can see that a restart is necessary
+ var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ var win = wm.getMostRecentWindow("Extension:Manager");
+
+ if (win) {
+ win.showView("themes");
+ return;
+ }
+
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ var param = Cc["@mozilla.org/supports-array;1"].
+ createInstance(Ci.nsISupportsArray);
+ var arg = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ arg.data = "themes";
+ param.AppendElement(arg);
+ ww.openWindow(null, URI_EXTENSION_MANAGER, null, FEATURES_EXTENSION_MANAGER, param);
+ return;
+ }
+ else {
+ // Cancel any pending theme change and allow the lightweight theme
+ // change to go ahead
+ if (gPref.prefHasUserValue(PREF_DSS_SWITCHPENDING))
+ gPref.clearUserPref(PREF_DSS_SWITCHPENDING);
+ if (gPref.prefHasUserValue(PREF_DSS_SKIN_TO_SELECT))
+ gPref.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
+ }
+ break;
+ case "xpcom-shutdown":
+ this._shutdown();
+ break;
+ case "nsPref:changed":
+ if (data == PREF_EM_LOGGING_ENABLED)
+ this._loggingToggled();
+ else if (data == gCheckCompatibilityPref ||
+ data == PREF_EM_CHECK_UPDATE_SECURITY)
+ this._updateAppDisabledState();
+ else if ((data == PREF_MATCH_OS_LOCALE) || (data == PREF_SELECTED_LOCALE))
+ this._updateLocale();
+ break;
+ }
+ },
+
+ /**
+ * Refresh the logging enabled global from preferences when the user changes
+ * the preference settting.
+ */
+ _loggingToggled: function EM__loggingToggled() {
+ gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
+ },
+
+ /**
+ * Retrieves the current locale
+ */
+ _updateLocale: function EM__updateLocale() {
+ try {
+ if (gPref.getBoolPref(PREF_MATCH_OS_LOCALE)) {
+ var localeSvc = Cc["@mozilla.org/intl/nslocaleservice;1"].
+ getService(Ci.nsILocaleService);
+ gLocale = localeSvc.getLocaleComponentForUserAgent();
+ return;
+ }
+ }
+ catch (ex) {
+ }
+ gLocale = gPref.getCharPref(PREF_SELECTED_LOCALE);
+ },
+
+ /**
+ * When a preference is toggled that affects whether an item is usable or not
+ * we must app-enable or app-disable the item based on the new settings.
+ */
+ _updateAppDisabledState: function EM__updateAppDisabledState() {
+ gCheckCompatibility = getPref("getBoolPref", gCheckCompatibilityPref, true);
+ gCheckUpdateSecurity = getPref("getBoolPref", PREF_EM_CHECK_UPDATE_SECURITY, true);
+ var ds = this.datasource;
+
+ // Enumerate all items
+ var ctr = getContainer(ds, ds._itemRoot);
+ var elements = ctr.GetElements();
+ while (elements.hasMoreElements()) {
+ var itemResource = elements.getNext().QueryInterface(Ci.nsIRDFResource);
+
+ // App disable or enable items as necessary
+ // _appEnableItem and _appDisableItem will do nothing if the item is already
+ // in the right state.
+ var id = stripPrefix(itemResource.Value, PREFIX_ITEM_URI);
+ if (this._isUsableItem(id))
+ this._appEnableItem(id);
+ else
+ this._appDisableItem(id);
+ }
+ },
+
+ /**
+ * Initialize the system after a profile has been selected.
+ */
+ _profileSelected: function EM__profileSelected() {
+ // Tell the Chrome Registry which Skin to select
+ try {
+ if (gPref.getBoolPref(PREF_DSS_SWITCHPENDING)) {
+ var toSelect = gPref.getCharPref(PREF_DSS_SKIN_TO_SELECT);
+ gPref.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, toSelect);
+ gPref.clearUserPref(PREF_DSS_SWITCHPENDING);
+ gPref.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
+
+ // If we've changed to a non-default theme make sure there is no
+ // lightweight theme selected
+ if (toSelect != gDefaultTheme) {
+ if (gPref.prefHasUserValue(PREF_LWTHEME_TO_SELECT))
+ gPref.clearUserPref(PREF_LWTHEME_TO_SELECT);
+ LightweightThemeManager.currentTheme = null;
+ }
+ }
+
+ if (gPref.prefHasUserValue(PREF_LWTHEME_TO_SELECT)) {
+ var id = gPref.getCharPref(PREF_LWTHEME_TO_SELECT);
+ if (id)
+ LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme(id);
+ else
+ LightweightThemeManager.currentTheme = null;
+ gPref.clearUserPref(PREF_LWTHEME_TO_SELECT);
+ }
+ }
+ catch (e) {
+ }
+
+ var version = gApp.version.replace(gBranchVersion, "$1");
+ gCheckCompatibilityPref = PREF_EM_CHECK_COMPATIBILITY + "." + version;
+
+ gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
+ gCheckCompatibility = getPref("getBoolPref", gCheckCompatibilityPref, true);
+ gCheckUpdateSecurity = getPref("getBoolPref", PREF_EM_CHECK_UPDATE_SECURITY, true);
+
+ if ("nsICrashReporter" in Ci && gApp instanceof Ci.nsICrashReporter) {
+ // Annotate the crash report with relevant add-on information.
+ try {
+ gApp.annotateCrashReport("Add-ons", gPref.getCharPref(PREF_EM_ENABLED_ITEMS));
+ } catch (e) { }
+ try {
+ gApp.annotateCrashReport("Theme", gPref.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN));
+ } catch (e) { }
+ try {
+ gApp.annotateCrashReport("EMCheckCompatibility", gCheckCompatibility);
+ } catch (e) { }
+ }
+
+ gPref.addObserver("extensions.", this, false);
+ gPref.addObserver(PREF_MATCH_OS_LOCALE, this, false);
+ gPref.addObserver(PREF_SELECTED_LOCALE, this, false);
+ this._updateLocale();
+ },
+
+ /**
+ * Notify user that there are new addons updates
+ */
+ _showUpdatesWindow: function EM__showUpdatesWindow() {
+ if (!getPref("getBoolPref", PREF_UPDATE_NOTIFYUSER, false))
+ return;
+
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ var param = Cc["@mozilla.org/supports-array;1"].
+ createInstance(Ci.nsISupportsArray);
+ var arg = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ arg.data = "updates-only";
+ param.AppendElement(arg);
+ ww.openWindow(null, URI_EXTENSION_MANAGER, null, FEATURES_EXTENSION_UPDATES, param);
+ },
+
+ /**
+ * Clean up on application shutdown to avoid leaks.
+ */
+ _shutdown: function EM__shutdown() {
+ if (!gAllowFlush) {
+ // Something went wrong and there are potentially flushes pending.
+ ERROR("Reached _shutdown and without clearing any pending flushes");
+ try {
+ gAllowFlush = true;
+ if (gManifestNeedsFlush) {
+ gManifestNeedsFlush = false;
+ this._updateManifests(false);
+ }
+ if (gDSNeedsFlush) {
+ gDSNeedsFlush = false;
+ this.datasource.Flush();
+ }
+ }
+ catch (e) {
+ ERROR("Error flushing caches: " + e);
+ }
+ }
+
+ gOS.removeObserver(this, "xpcom-shutdown");
+ gOS.removeObserver(this, "lightweight-theme-preview-requested");
+ gOS.removeObserver(this, "lightweight-theme-change-requested");
+
+ // Release strongly held services.
+ gOS = null;
+ if (this._ds) {
+ gRDF.UnregisterDataSource(this._ptr);
+ this._ptr = null;
+ this._ds.shutdown();
+ this._ds = null;
+ }
+ gRDF = null;
+ if (gPref) {
+ gPref.removeObserver("extensions.", this);
+ gPref.removeObserver(PREF_MATCH_OS_LOCALE, this);
+ gPref.removeObserver(PREF_SELECTED_LOCALE, this);
+ }
+ gPref = null;
+ gConsole = null;
+ gVersionChecker = null;
+ gInstallManifestRoot = null;
+ gApp = null;
+ },
+
+ /**
+ * Check for presence of critical Extension system files. If any is missing,
+ * delete the others and signal that the system needs to rebuild them all
+ * from scratch.
+ * @returns true if any critical file is missing and the system needs to
+ * be rebuilt, false otherwise.
+ */
+ _ensureDatasetIntegrity: function EM__ensureDatasetIntegrity() {
+ var profD = FileUtils.getDir(KEY_PROFILEDIR, [], false);
+ var extensionsDS = profD.clone();
+ extensionsDS.append(FILE_EXTENSIONS);
+ var extensionsINI = profD.clone();
+ extensionsINI.append(FILE_EXTENSION_MANIFEST);
+ var extensionsCache = profD;
+ extensionsCache.append(FILE_EXTENSIONS_STARTUP_CACHE);
+
+ var dsExists = extensionsDS.exists();
+ var iniExists = extensionsINI.exists();
+ var cacheExists = extensionsCache.exists();
+
+ if (dsExists && iniExists && cacheExists)
+ return [false, !iniExists];
+
+ // If any of the files are missing, remove the .ini file
+ if (iniExists)
+ extensionsINI.remove(false);
+
+ // If the extensions datasource is missing remove the .cache file if it exists
+ if (!dsExists && cacheExists)
+ extensionsCache.remove(false);
+
+ return [true, !iniExists];
+ },
+
+ /**
+ * See nsIExtensionManager.idl
+ */
+ start: function EM_start() {
+ var isDirty, forceAutoReg;
+
+ // Check for missing manifests - e.g. missing extensions.ini, missing
+ // extensions.cache, extensions.rdf etc. If any of these files
+ // is missing then we are in some kind of weird or initial state and need
+ // to force a regeneration.
+ [isDirty, forceAutoReg] = this._ensureDatasetIntegrity();
+
+ // Block attempts to flush for the entire startup
+ gAllowFlush = false;
+
+ // Configure any items that are being installed, uninstalled or upgraded
+ // by being added, removed or modified by another process. We must do this
+ // on every startup since there is no way we can tell if this has happened
+ // or not!
+ if (this._checkForFileChanges())
+ isDirty = true;
+
+ this._showUpdatesWindow();
+
+ if (PendingOperations.size != 0)
+ isDirty = true;
+
+ var needsRestart = false;
+ // Extension Changes
+ if (isDirty) {
+ needsRestart = this._finishOperations();
+
+ if (forceAutoReg) {
+ this._extensionListChanged = true;
+ needsRestart = true;
+ }
+ }
+
+ // Resume flushing and perform a flush for anything that was deferred
+ try {
+ gAllowFlush = true;
+ if (gManifestNeedsFlush) {
+ gManifestNeedsFlush = false;
+ this._updateManifests(false);
+ }
+ if (gDSNeedsFlush) {
+ gDSNeedsFlush = false;
+ this.datasource.Flush();
+ }
+ }
+ catch (e) {
+ ERROR("Error flushing caches: " + e);
+ }
+
+ return needsRestart;
+ },
+
+ /**
+ * Notified when a timer fires
+ * @param timer
+ * The timer that fired
+ */
+ notify: function EM_notify(timer) {
+ if (!getPref("getBoolPref", PREF_EM_UPDATE_ENABLED, true))
+ return;
+
+ var items = this.getItemList(Ci.nsIUpdateItem.TYPE_ANY);
+
+ var updater = new ExtensionItemUpdater(this);
+ updater.checkForUpdates(items, items.length,
+ Ci.nsIExtensionManager.UPDATE_CHECK_NEWVERSION,
+ new BackgroundUpdateCheckListener(this.datasource),
+ UPDATE_WHEN_PERIODIC_UPDATE);
+
+ LightweightThemeManager.updateCurrentTheme();
+ },
+
+ /**
+ * Check to see if a file is a XPI/JAR file that the user dropped into this
+ * Install Location. (i.e. a XPI that is not a staged XPI from an install
+ * transaction that is currently in operation).
+ * @param file
+ * The XPI/JAR file to configure
+ * @param location
+ * The Install Location where this file was found.
+ * @returns A nsIUpdateItem representing the dropped XPI if this file was a
+ * XPI/JAR that needs installation, null otherwise.
+ */
+ _getItemForDroppedFile: function EM__getItemForDroppedFile(file, location) {
+ if (fileIsItemPackage(file)) {
+ // We know nothing about this item, it is not something we've
+ // staged in preparation for finalization, so assume it's something
+ // the user dropped in.
+ LOG("A Item Package appeared at: " + file.path + " that we know " +
+ "nothing about, assuming it was dropped in by the user and " +
+ "configuring for installation now. Location Key: " + location.name);
+
+ var installManifestFile = extractRDFFileToTempDir(file, FILE_INSTALL_MANIFEST, true);
+ if (!installManifestFile.exists())
+ return null;
+ var installManifest = getInstallManifest(installManifestFile);
+ installManifestFile.remove(false);
+ var ds = this.datasource;
+ var installData = this._getInstallData(installManifest);
+ var targetAppInfo = ds.getTargetApplicationInfo(installData.id, installManifest);
+ return makeItem(installData.id,
+ installData.version,
+ location.name,
+ targetAppInfo ? targetAppInfo.minVersion : "",
+ targetAppInfo ? targetAppInfo.maxVersion : "",
+ getManifestProperty(installManifest, "name"),
+ "", /* XPI Update URL */
+ "", /* XPI Update Hash */
+ getManifestProperty(installManifest, "iconURL"),
+ getManifestProperty(installManifest, "updateURL"),
+ getManifestProperty(installManifest, "updateKey"),
+ installData.type,
+ targetAppInfo ? targetAppInfo.appID : gApp.ID);
+ }
+ return null;
+ },
+
+ /**
+ * Configure an item that was installed or upgraded by another process
+ * so that |_finishOperations| can properly complete processing and
+ * registration.
+ * As this is the only point at which we can reliably know the Install
+ * Location of this item, we use this as an opportunity to:
+ * 1. Check that this item is compatible with this Firefox version.
+ * 2. If it is, configure the item by using the supplied callback.
+ * We do not do any special handling in the case that the item is
+ * not compatible with this version other than to simply not register
+ * it and log that fact - there is no "phone home" check for updates.
+ * It may or may not make sense to do this, but for now we'll just
+ * not register.
+ * @param id
+ * The GUID of the item to validate and configure.
+ * @param location
+ * The Install Location where this item is installed.
+ * @param callback
+ * The callback that configures the item for installation upon
+ * successful validation.
+ */
+ installItem: function EM_installItem(id, location, callback) {
+ // As this is the only pint at which we reliably know the installation
+ var installRDF = location.getItemFile(id, FILE_INSTALL_MANIFEST);
+ if (installRDF.exists()) {
+ LOG("Item Installed/Upgraded at Install Location: " + location.name +
+ " Item ID: " + id + ", attempting to register...");
+ var installManifest = getInstallManifest(installRDF);
+ var installData = this._getInstallData(installManifest);
+ if (installData.error == INSTALLERROR_SUCCESS) {
+ LOG("... success, item is compatible");
+ callback(installManifest, installData.id, location, installData.type);
+ }
+ else if (installData.error == INSTALLERROR_INCOMPATIBLE_VERSION) {
+ LOG("... success, item installed but is not compatible");
+ callback(installManifest, installData.id, location, installData.type);
+ this._appDisableItem(id);
+ }
+ else if (installData.error == INSTALLERROR_INSECURE_UPDATE) {
+ LOG("... success, item installed but does not provide updates securely");
+ callback(installManifest, installData.id, location, installData.type);
+ this._appDisableItem(id);
+ }
+ else if (installData.error == INSTALLERROR_BLOCKLISTED) {
+ LOG("... success, item installed but is blocklisted");
+ callback(installManifest, installData.id, location, installData.type);
+ this._appDisableItem(id);
+ }
+ else if (installData.error == INSTALLERROR_SOFTBLOCKED) {
+ LOG("... success, item installed but is soft blocked, item will be disabled");
+ callback(installManifest, installData.id, location, installData.type);
+ this.disableItem(id);
+ }
+ else {
+ /**
+ * Turns an error code into a message for logging
+ * @param error
+ * an Install Error code
+ * @returns A string message to be logged.
+ */
+ function translateErrorMessage(error) {
+ switch (error) {
+ case INSTALLERROR_INVALID_GUID:
+ return "Invalid GUID";
+ case INSTALLERROR_INVALID_VERSION:
+ return "Invalid Version";
+ case INSTALLERROR_INCOMPATIBLE_PLATFORM:
+ return "Incompatible Platform";
+ }
+ }
+ LOG("... failure, item is not compatible, error: " +
+ translateErrorMessage(installData.error));
+
+ // Add the item to the Startup Cache anyway, so we don't re-detect it
+ // every time the app starts.
+ StartupCache.put(location, id, OP_NONE, true);
+ }
+ }
+ },
+
+ /**
+ * Check for changes to items that were made independently of the Extension
+ * Manager, e.g. items were added or removed from a Install Location or items
+ * in an Install Location changed.
+ */
+ _checkForFileChanges: function EM__checkForFileChanges() {
+ var em = this;
+
+ /**
+ * Determines if an item can be used based on whether or not the install
+ * location of the "item" has an equal or higher priority than the install
+ * location where another version may live.
+ * @param id
+ * The GUID of the item being installed.
+ * @param location
+ * The location where an item is to be installed.
+ * @returns true if the item can be installed at that location, false
+ * otherwise.
+ */
+ function canUse(id, location) {
+ for (var locationKey in StartupCache.entries) {
+ if (locationKey != location.name &&
+ id in StartupCache.entries[locationKey]) {
+ if (StartupCache.entries[locationKey][id]) {
+ var oldInstallLocation = InstallLocations.get(locationKey);
+ if (oldInstallLocation.priority <= location.priority)
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Gets a Dialog Param Block loaded with a set of strings to initialize the
+ * XPInstall Confirmation Dialog.
+ * @param strings
+ * An array of strings
+ * @returns A nsIDialogParamBlock loaded with the strings and dialog state.
+ */
+ function getParamBlock(strings) {
+ var dpb = Cc["@mozilla.org/embedcomp/dialogparam;1"].
+ createInstance(Ci.nsIDialogParamBlock);
+ // OK and Cancel Buttons
+ dpb.SetInt(0, 2);
+ // Number of Strings
+ dpb.SetInt(1, strings.length);
+ dpb.SetNumberStrings(strings.length);
+ // Add Strings
+ for (var i = 0; i < strings.length; ++i)
+ dpb.SetString(i, strings[i]);
+
+ var supportsString = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ var bundle = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
+ supportsString.data = bundle.GetStringFromName("droppedInWarning");
+ var objs = Cc["@mozilla.org/array;1"].
+ createInstance(Ci.nsIMutableArray);
+ objs.appendElement(supportsString, false);
+ dpb.objects = objs;
+ return dpb;
+ }
+
+ /**
+ * Installs a set of files which were dropped into an install location by
+ * the user, only after user confirmation.
+ * @param droppedInFiles
+ * An array of JS objects with the following properties:
+ * "file" The nsILocalFile where the XPI lives
+ * "location" The Install Location where the XPI was found.
+ * @param xpinstallStrings
+ * An array of strings used to initialize the XPInstall Confirm
+ * dialog.
+ */
+ function installDroppedInFiles(droppedInFiles, xpinstallStrings) {
+ if (droppedInFiles.length == 0)
+ return;
+
+ var dpb = getParamBlock(xpinstallStrings);
+ var ifptr = Cc["@mozilla.org/supports-interface-pointer;1"].
+ createInstance(Ci.nsISupportsInterfacePointer);
+ ifptr.data = dpb;
+ ifptr.dataIID = Ci.nsIDialogParamBlock;
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ ww.openWindow(null, URI_XPINSTALL_CONFIRM_DIALOG,
+ "", "chrome,centerscreen,modal,dialog,titlebar", ifptr);
+ if (!dpb.GetInt(0)) {
+ // User said OK - install items
+ for (var i = 0; i < droppedInFiles.length; ++i) {
+ em.installItemFromFile(droppedInFiles[i].file,
+ droppedInFiles[i].location.name);
+ // We are responsible for cleaning up this file
+ droppedInFiles[i].file.remove(false);
+ }
+ }
+ else {
+ for (i = 0; i < droppedInFiles.length; ++i) {
+ // We are responsible for cleaning up this file
+ droppedInFiles[i].file.remove(false);
+ }
+ }
+ }
+
+ var isDirty = false;
+ var ignoreMTimeChanges = getPref("getBoolPref", PREF_EM_IGNOREMTIMECHANGES,
+ false);
+ StartupCache.read();
+
+ // Array of objects with 'location' and 'id' properties to maybe install.
+ var newItems = [];
+
+ var droppedInFiles = [];
+ var xpinstallStrings = [];
+
+ // Enumerate over the install locations from low to high priority. The
+ // enumeration returned is pre-sorted.
+ var installLocations = this.installLocations;
+ while (installLocations.hasMoreElements()) {
+ var location = installLocations.getNext().QueryInterface(Ci.nsIInstallLocation);
+
+ // Hash the set of items actually held by the Install Location.
+ var actualItems = { };
+ var entries = location.itemLocations;
+ while (true) {
+ var entry = entries.nextFile;
+ if (!entry)
+ break;
+
+ // Is this location a valid item? It must be a directory, and contain
+ // an install.rdf manifest:
+ if (entry.isDirectory()) {
+ var installRDF = entry.clone();
+ installRDF.append(FILE_INSTALL_MANIFEST);
+
+ var id = location.getIDForLocation(entry);
+ if (!id || (!installRDF.exists() &&
+ !location.itemIsManagedIndependently(id)))
+ continue;
+
+ actualItems[id] = entry;
+ }
+ else {
+ // Check to see if this file is a XPI/JAR dropped into this dir
+ // by the user, installing it if necessary. We do this here rather
+ // than separately in |_finishOperations| because I don't want to
+ // walk these lists multiple times on every startup.
+ var item = this._getItemForDroppedFile(entry, location);
+ if (item) {
+ var prettyName = "";
+ try {
+ var zipReader = getZipReaderForFile(entry);
+ zipReader.QueryInterface(Ci.nsIJAR);
+ var principal = zipReader.getCertificatePrincipal(null);
+ if (principal && principal.hasCertificate) {
+ if (verifyZipSigning(zipReader, principal)) {
+ var x509 = principal.certificate;
+ if (x509 instanceof Ci.nsIX509Cert && x509.commonName.length > 0)
+ prettyName = x509.commonName;
+ else
+ prettyName = principal.prettyName;
+ }
+ else {
+ // The xpi isn't correctly signed, don't offer to install.
+ LOG("Ignoring " + entry.path + " as it is not correctly signed.");
+ zipReader.close();
+ entry.remove(true);
+ continue;
+ }
+ }
+ }
+ catch (e) { }
+ if (zipReader)
+ zipReader.close();
+ droppedInFiles.push({ file: entry, location: location });
+ xpinstallStrings = xpinstallStrings.concat([item.name,
+ getURLSpecFromFile(entry),
+ item.iconURL,
+ prettyName]);
+ isDirty = true;
+ }
+ }
+ }
+
+ if (location.name in StartupCache.entries) {
+ // Look for items that have been uninstalled by removing their directory.
+ for (var id in StartupCache.entries[location.name]) {
+ if (!StartupCache.entries[location.name] ||
+ !StartupCache.entries[location.name][id])
+ continue;
+
+ // Force _finishOperations to run if we have enabled or disabled items.
+ // XXXdarin this should be unnecessary now that we check
+ // PendingOperations.size in start()
+ if (StartupCache.entries[location.name][id].op == OP_NEEDS_ENABLE ||
+ StartupCache.entries[location.name][id].op == OP_NEEDS_DISABLE)
+ isDirty = true;
+
+ if (!(id in actualItems) &&
+ StartupCache.entries[location.name][id].op != OP_NEEDS_UNINSTALL &&
+ StartupCache.entries[location.name][id].op != OP_NEEDS_INSTALL &&
+ StartupCache.entries[location.name][id].op != OP_NEEDS_UPGRADE) {
+ // We have an entry for this id in the Extensions database, for this
+ // install location, but it no longer exists in the Install Location.
+ // We can infer from this that the item has been removed, so uninstall
+ // it properly.
+ if (canUse(id, location)) {
+ LOG("Item Uninstalled via file removal from: " + StartupCache.entries[location.name][id].descriptor +
+ " Item ID: " + id + " Location Key: " + location.name + ", uninstalling item.");
+
+ // Load the Extensions Datasource and force this item into the visible
+ // items list if it is not already. This allows us to handle the case
+ // where there is an entry for an item in the Startup Cache but not
+ // in the extensions.rdf file - in that case the item will not be in
+ // the visible list and calls to |getInstallLocation| will mysteriously
+ // fail.
+ this.datasource.updateVisibleList(id, location.name, false);
+ this.uninstallItem(id);
+ isDirty = true;
+ }
+ }
+ else if (!ignoreMTimeChanges) {
+ // Look for items whose mtime has changed, and as such we can assume
+ // they have been "upgraded".
+ var lf = { path: StartupCache.entries[location.name][id].descriptor };
+ try {
+ lf = getFileFromDescriptor(StartupCache.entries[location.name][id].descriptor, location);
+ }
+ catch (e) { }
+
+ if (lf.exists && lf.exists()) {
+ var actualMTime = Math.floor(lf.lastModifiedTime / 1000);
+ if (actualMTime != StartupCache.entries[location.name][id].mtime) {
+ LOG("Item Location path changed: " + lf.path + " Item ID: " +
+ id + " Location Key: " + location.name + ", attempting to upgrade item...");
+ if (canUse(id, location)) {
+ this.installItem(id, location,
+ function(installManifest, id, location, type) {
+ em._upgradeItem(installManifest, id, location,
+ type);
+ });
+ isDirty = true;
+ }
+ }
+ }
+ else {
+ isDirty = true;
+ LOG("Install Location returned a missing or malformed item path! " +
+ "Item Path: " + lf.path + ", Location Key: " + location.name +
+ " Item ID: " + id);
+ if (canUse(id, location)) {
+ // Load the Extensions Datasource and force this item into the visible
+ // items list if it is not already. This allows us to handle the case
+ // where there is an entry for an item in the Startup Cache but not
+ // in the extensions.rdf file - in that case the item will not be in
+ // the visible list and calls to |getInstallLocation| will mysteriously
+ // fail.
+ this.datasource.updateVisibleList(id, location.name, false);
+ this.uninstallItem(id);
+ }
+ }
+ }
+ }
+ }
+
+ // Look for items that have been installed by appearing in the location.
+ for (var id in actualItems) {
+ if (!(location.name in StartupCache.entries) ||
+ !(id in StartupCache.entries[location.name]) ||
+ !StartupCache.entries[location.name][id]) {
+ // Remember that we've seen this item
+ StartupCache.put(location, id, OP_NONE, true);
+ // Push it on the stack of items to maybe install later
+ newItems.push({location: location, id: id});
+ }
+ }
+ }
+
+ // Process any newly discovered items. We do this here instead of in the
+ // previous loop so that we can be sure that we have a fully populated
+ // StartupCache.
+ for (var i = 0; i < newItems.length; ++i) {
+ var id = newItems[i].id;
+ var location = newItems[i].location;
+ if (canUse(id, location)) {
+ LOG("Item Installed via directory addition to Install Location: " +
+ location.name + " Item ID: " + id + ", attempting to register...");
+ this.installItem(id, location,
+ function(installManifest, id, location, type) {
+ em._configureForthcomingItem(installManifest, id, location,
+ type);
+ });
+ // Disable add-ons on install when the InstallDisabled file exists.
+ // This is so Talkback will be disabled on a subset of installs.
+ var installDisabled = location.getItemFile(id, "InstallDisabled");
+ if (installDisabled.exists())
+ em.disableItem(id);
+ isDirty = true;
+ }
+ }
+
+ // Ask the user if they want to install the dropped items, for security
+ // purposes.
+ installDroppedInFiles(droppedInFiles, xpinstallStrings);
+
+ return isDirty;
+ },
+
+ _checkForUncoveredItem: function EM__checkForUncoveredItem(id) {
+ var ds = this.datasource;
+ var oldLocation = this.getInstallLocation(id);
+ var newLocations = [];
+ for (var locationKey in StartupCache.entries) {
+ var location = InstallLocations.get(locationKey);
+ if (id in StartupCache.entries[locationKey] &&
+ location.priority > oldLocation.priority)
+ newLocations.push(location);
+ }
+ newLocations.sort(function(a, b) { return b.priority - a.priority; });
+ if (newLocations.length > 0) {
+ for (var i = 0; i < newLocations.length; ++i) {
+ // Check to see that the item at the location exists
+ var installRDF = newLocations[i].getItemFile(id, FILE_INSTALL_MANIFEST);
+ if (installRDF.exists()) {
+ // Update the visible item cache so that |_finalizeUpgrade| is properly
+ // called from |_finishOperations|
+ var name = newLocations[i].name;
+ ds.updateVisibleList(id, name, true);
+ PendingOperations.addItem(OP_NEEDS_UPGRADE,
+ { locationKey: name, id: id });
+ PendingOperations.addItem(OP_NEEDS_INSTALL,
+ { locationKey: name, id: id });
+ break;
+ }
+ else {
+ // If no item exists at the location specified, remove this item
+ // from the visible items list and check again.
+ StartupCache.clearEntry(newLocations[i], id);
+ ds.updateVisibleList(id, null, true);
+ }
+ }
+ }
+ else
+ ds.updateVisibleList(id, null, true);
+ },
+
+ /**
+ * Finish up pending operations - perform upgrades, installs, enables/disables,
+ * uninstalls etc.
+ * @returns true if actions were performed that require a restart, false
+ * otherwise.
+ */
+ _finishOperations: function EM__finishOperations() {
+ try {
+ // Stuff has changed, load the Extensions datasource in all its RDFey
+ // glory.
+ var ds = this.datasource;
+ var updatedTargetAppInfos = [];
+
+ var needsRestart = false;
+ var upgrades = [];
+ var newAddons = [];
+ var addons = getPref("getCharPref", PREF_EM_NEW_ADDONS_LIST, "");
+ if (addons != "")
+ newAddons = addons.split(",");
+ do {
+ // Enable and disable during startup so items that are changed in the
+ // ui can be reset to a no-op.
+ // Look for extensions that need to be enabled.
+ var items = PendingOperations.getOperations(OP_NEEDS_ENABLE);
+ for (var i = items.length - 1; i >= 0; --i) {
+ var id = items[i].id;
+ var installLocation = this.getInstallLocation(id);
+ StartupCache.put(installLocation, id, OP_NONE, true);
+ PendingOperations.clearItem(OP_NEEDS_ENABLE, id);
+ needsRestart = true;
+ }
+ PendingOperations.clearItems(OP_NEEDS_ENABLE);
+
+ // Look for extensions that need to be disabled.
+ items = PendingOperations.getOperations(OP_NEEDS_DISABLE);
+ for (i = items.length - 1; i >= 0; --i) {
+ id = items[i].id;
+ installLocation = this.getInstallLocation(id);
+ StartupCache.put(installLocation, id, OP_NONE, true);
+ PendingOperations.clearItem(OP_NEEDS_DISABLE, id);
+ needsRestart = true;
+ }
+ PendingOperations.clearItems(OP_NEEDS_DISABLE);
+
+ // Look for extensions that need to be upgraded. The process here is to
+ // uninstall the old version of the extension first, then install the
+ // new version in its place.
+ items = PendingOperations.getOperations(OP_NEEDS_UPGRADE);
+ for (i = items.length - 1; i >= 0; --i) {
+ id = items[i].id;
+ var newLocation = InstallLocations.get(items[i].locationKey);
+ // check if there is updated app compatibility info
+ var newTargetAppInfo = ds.getUpdatedTargetAppInfo(id);
+ if (newTargetAppInfo)
+ updatedTargetAppInfos.push(newTargetAppInfo);
+ this._finalizeUpgrade(id, newLocation);
+ upgrades.push(id);
+ }
+ PendingOperations.clearItems(OP_NEEDS_UPGRADE);
+
+ // Install items
+ items = PendingOperations.getOperations(OP_NEEDS_INSTALL);
+ for (i = items.length - 1; i >= 0; --i) {
+ needsRestart = true;
+ id = items[i].id;
+ // check if there is updated app compatibility info
+ newTargetAppInfo = ds.getUpdatedTargetAppInfo(id);
+ if (newTargetAppInfo)
+ updatedTargetAppInfos.push(newTargetAppInfo);
+ this._finalizeInstall(id, null);
+ if (upgrades.indexOf(id) < 0 && newAddons.indexOf(id) < 0)
+ newAddons.push(id);
+ }
+ PendingOperations.clearItems(OP_NEEDS_INSTALL);
+
+ // Look for extensions that need to be removed. This MUST be done after
+ // the install operations since extensions to be installed may have to be
+ // uninstalled if there are errors during the installation process!
+ items = PendingOperations.getOperations(OP_NEEDS_UNINSTALL);
+ for (i = items.length - 1; i >= 0; --i) {
+ id = items[i].id;
+ this._finalizeUninstall(id);
+ this._checkForUncoveredItem(id);
+ needsRestart = true;
+ var pos = newAddons.indexOf(id);
+ if (pos >= 0)
+ newAddons.splice(pos, 1);
+ }
+ PendingOperations.clearItems(OP_NEEDS_UNINSTALL);
+
+ // When there have been operations and all operations have completed.
+ if (PendingOperations.size == 0) {
+ // If there is updated app compatibility info update the datasource.
+ for (i = 0; i < updatedTargetAppInfos.length; ++i)
+ ds.setTargetApplicationInfo(updatedTargetAppInfos[i].id,
+ updatedTargetAppInfos[i].targetAppID,
+ updatedTargetAppInfos[i].minVersion,
+ updatedTargetAppInfos[i].maxVersion,
+ null);
+
+ // Enumerate all items
+ var ctr = getContainer(ds, ds._itemRoot);
+ var elements = ctr.GetElements();
+ while (elements.hasMoreElements()) {
+ var itemResource = elements.getNext().QueryInterface(Ci.nsIRDFResource);
+
+ // Ensure appDisabled is in the correct state.
+ id = stripPrefix(itemResource.Value, PREFIX_ITEM_URI);
+ if (this._isUsableItem(id))
+ ds.setItemProperty(id, EM_R("appDisabled"), null);
+ else
+ ds.setItemProperty(id, EM_R("appDisabled"), EM_L("true"));
+
+ // userDisabled is set based on its value being OP_NEEDS_ENABLE or
+ // OP_NEEDS_DISABLE. This allows us to have an item to be enabled
+ // by the app and disabled by the user during a single restart.
+ var value = stringData(ds.GetTarget(itemResource, EM_R("userDisabled"), true));
+ if (value == OP_NEEDS_ENABLE)
+ ds.setItemProperty(id, EM_R("userDisabled"), null);
+ else if (value == OP_NEEDS_DISABLE)
+ ds.setItemProperty(id, EM_R("userDisabled"), EM_L("true"));
+ }
+ }
+ }
+ while (PendingOperations.size > 0);
+
+ // If no additional restart is required, it implies that there are
+ // no new components that need registering so we can inform the app
+ // not to do any extra startup checking next time round.
+ this._updateManifests(needsRestart);
+
+ // Remember the list of add-ons that were installed this time around
+ // unless this was a new profile.
+ if (!gFirstRun && newAddons.length > 0)
+ gPref.setCharPref(PREF_EM_NEW_ADDONS_LIST, newAddons.join(","));
+ }
+ catch (e) {
+ ERROR("ExtensionManager:_finishOperations - failure, catching exception - lineno: " +
+ e.lineNumber + " - file: " + e.fileName + " - " + e);
+ }
+ return needsRestart;
+ },
+
+ /**
+ * Checks to see if there are items that are incompatible with this version
+ * of the application, disables them to prevent incompatibility problems and
+ * invokes the Update Wizard to look for newer versions.
+ * @returns true if there were incompatible items installed and disabled, and
+ * the application must now be restarted to reinitialize XPCOM,
+ * false otherwise.
+ */
+ checkForMismatches: function EM_checkForMismatches() {
+ // Check to see if the version of the application that is being started
+ // now is the same one that was started last time.
+ var currAppVersion = gApp.version;
+ var lastAppVersion = getPref("getCharPref", PREF_EM_LAST_APP_VERSION, "");
+ if (currAppVersion == lastAppVersion)
+ return false;
+ // With a new profile lastAppVersion doesn't exist yet.
+ if (!lastAppVersion) {
+ gPref.setCharPref(PREF_EM_LAST_APP_VERSION, currAppVersion);
+ return false;
+ }
+
+ // Block attempts to flush for the entire startup
+ gAllowFlush = false;
+
+ // Make the extensions datasource consistent if it isn't already.
+ var isDirty;
+ [isDirty,] = this._ensureDatasetIntegrity();
+
+ if (this._checkForFileChanges())
+ isDirty = true;
+
+ if (PendingOperations.size != 0)
+ isDirty = true;
+
+ var ds = this.datasource;
+ var inactiveItemIDs = [];
+ var ctr = getContainer(ds, ds._itemRoot);
+ var elements = ctr.GetElements();
+ while (elements.hasMoreElements()) {
+ var itemResource = elements.getNext().QueryInterface(Ci.nsIRDFResource);
+ var id = stripPrefix(itemResource.Value, PREFIX_ITEM_URI);
+ var appDisabled = ds.getItemProperty(id, "appDisabled");
+ var userDisabled = ds.getItemProperty(id, "userDisabled")
+ if (appDisabled == "true" || appDisabled == OP_NEEDS_DISABLE ||
+ userDisabled == "true" || userDisabled == OP_NEEDS_DISABLE)
+ inactiveItemIDs.push(id);
+ }
+
+ if (isDirty)
+ this._finishOperations();
+
+ // During app upgrade cleanup invalid entries in the extensions datasource.
+ ds.beginUpdateBatch();
+ var allResources = ds.GetAllResources();
+ while (allResources.hasMoreElements()) {
+ var res = allResources.getNext().QueryInterface(Ci.nsIRDFResource);
+ if (ds.GetTarget(res, EM_R("downloadURL"), true) ||
+ (!ds.GetTarget(res, EM_R("installLocation"), true) &&
+ stringData(ds.GetTarget(res, EM_R("appDisabled"), true)) == "true"))
+ ds.removeDownload(res.Value);
+ }
+ ds.endUpdateBatch();
+
+ var badItems = [];
+ var disabledAddons = [];
+ var allAppManaged = true;
+ elements = ctr.GetElements();
+ while (elements.hasMoreElements()) {
+ var itemResource = elements.getNext().QueryInterface(Ci.nsIRDFResource);
+ var id = stripPrefix(itemResource.Value, PREFIX_ITEM_URI);
+ var location = this.getInstallLocation(id);
+ if (!location) {
+ // Item was in an unknown install location
+ badItems.push(id);
+ continue;
+ }
+
+ if (ds.getItemProperty(id, "appManaged") == "true") {
+ // Force an update of the metadata for appManaged extensions since the
+ // last modified time is not updated for directories on FAT / FAT32
+ // filesystems when software update applies a new version of the app.
+ if (location.name == KEY_APP_GLOBAL) {
+ var installRDF = location.getItemFile(id, FILE_INSTALL_MANIFEST);
+ if (installRDF.exists()) {
+ var metadataDS = getInstallManifest(installRDF);
+ ds.addItemMetadata(id, metadataDS, location);
+ ds.updateProperty(id, "compatible");
+ }
+ }
+ }
+ else if (allAppManaged)
+ allAppManaged = false;
+
+ var properties = {
+ availableUpdateURL: null,
+ availableUpdateVersion: null
+ };
+
+ if (ds.getItemProperty(id, "providesUpdatesSecurely") == "false") {
+ /* It's possible the previous version did not understand updateKeys so
+ * check if we can import one for this addon from its manifest. */
+ installRDF = location.getItemFile(id, FILE_INSTALL_MANIFEST);
+ if (installRDF.exists()) {
+ metadataDS = getInstallManifest(installRDF);
+ var literal = metadataDS.GetTarget(gInstallManifestRoot, EM_R("updateKey"), true);
+ if (literal && literal instanceof Ci.nsIRDFLiteral)
+ ds.setItemProperty(id, EM_R("updateKey"), literal);
+ }
+ }
+
+ // appDisabled is determined by an item being compatible, using secure
+ // updates, satisfying its dependencies, and not being blocklisted
+ if (this._isUsableItem(id)) {
+ if (ds.getItemProperty(id, "appDisabled"))
+ properties.appDisabled = null;
+ }
+ else if (!ds.getItemProperty(id, "appDisabled")) {
+ properties.appDisabled = EM_L("true");
+ disabledAddons.push(id);
+ }
+
+ ds.setItemProperties(id, properties);
+ }
+
+ // Must clean up outside of the loop. Modifying the container while
+ // iterating its contents is bad.
+ for (var i = 0; i < badItems.length; i++) {
+ id = badItems[i];
+ LOG("Item " + id + " was installed in an unknown location, removing.");
+ var disabled = ds.getItemProperty(id, "userDisabled") == "true";
+ // Clean up the datasource
+ ds.removeCorruptItem(id);
+ // Check for any unhidden items.
+ var entries = StartupCache.findEntries(id);
+ if (entries.length > 0) {
+ var newLocation = InstallLocations.get(entries[0].location);
+ for (var j = 1; j < entries.length; j++) {
+ location = InstallLocations.get(entries[j].location);
+ if (newLocation.priority < location.priority)
+ newLocation = location;
+ }
+ LOG("Activating item " + id + " in " + newLocation.name);
+ var em = this;
+ this.installItem(id, newLocation,
+ function(installManifest, id, location, type) {
+ em._configureForthcomingItem(installManifest, id, location,
+ type);
+ });
+ if (disabled)
+ em.disableItem(id);
+ }
+ }
+
+ // Update the manifests to reflect the items that were disabled / enabled.
+ this._updateManifests(true);
+
+ // Determine if we should check for compatibility updates when upgrading if
+ // we have add-ons that aren't managed by the application.
+ if (!allAppManaged && !gFirstRun && disabledAddons.length > 0) {
+ // Should we show a UI or just pass the list via a pref?
+ if (getPref("getBoolPref", PREF_EM_SHOW_MISMATCH_UI, true)) {
+ this._showMismatchWindow(inactiveItemIDs);
+ }
+ else {
+ // Remember the list of add-ons that were disabled this time around
+ gPref.setCharPref(PREF_EM_DISABLED_ADDONS_LIST, disabledAddons.join(","));
+ }
+ } else if (gPref.prefHasUserValue(PREF_EM_DISABLED_ADDONS_LIST)) {
+ // Clear the disabled addons list if necessary
+ gPref.clearUserPref(PREF_EM_DISABLED_ADDONS_LIST);
+ }
+
+ // Finish any pending upgrades from the compatibility update to avoid an
+ // additional restart.
+ if (PendingOperations.size != 0)
+ this._finishOperations();
+
+ // Update the last app version so we don't do this again with this version.
+ gPref.setCharPref(PREF_EM_LAST_APP_VERSION, currAppVersion);
+
+ // Prevent extension update dialog from showing
+ gPref.setBoolPref(PREF_UPDATE_NOTIFYUSER, false);
+
+ // Re-enable flushing and flush anything that was deferred
+ try {
+ gAllowFlush = true;
+ if (gManifestNeedsFlush) {
+ gManifestNeedsFlush = false;
+ this._updateManifests(false);
+ }
+ if (gDSNeedsFlush) {
+ gDSNeedsFlush = false;
+ this.datasource.Flush();
+ }
+ }
+ catch (e) {
+ ERROR("Error flushing caches: " + e);
+ }
+
+ return true;
+ },
+
+ /**
+ * Shows the "Compatibility Updates" UI
+ * @param items
+ * an array of item IDs that were not enabled in the previous version
+ * of the application.
+ */
+ _showMismatchWindow: function EM__showMismatchWindow(items) {
+ var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ var wizard = wm.getMostRecentWindow("Update:Wizard");
+ if (wizard)
+ wizard.focus();
+ else {
+ var variant = Cc["@mozilla.org/variant;1"].
+ createInstance(Ci.nsIWritableVariant);
+ variant.setFromVariant(items);
+ var features = "chrome,centerscreen,dialog,titlebar,modal";
+ // This *must* be modal so as not to break startup! This code is invoked before
+ // the main event loop is initiated (via checkForMismatches).
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
+ }
+ },
+
+ /**
+ * Write the Extensions List and the Startup Cache
+ * @param needsRestart
+ * true if the application needs to restart again, false otherwise.
+ */
+ _updateManifests: function EM__updateManifests(needsRestart) {
+ // During startup we block flushing until the startup operations are all
+ // complete to reduce file accesses that can trigger bug 431065
+ if (gAllowFlush) {
+ // Write the Startup Cache (All Items, visible or not)
+ StartupCache.write();
+ // Write the Extensions Locations Manifest (Visible, enabled items)
+ this._updateExtensionsManifest();
+ }
+ else {
+ gManifestNeedsFlush = true;
+ }
+
+ // Notify nsAppRunner to update the compatibility manifest on next startup
+ this._extensionListChanged = needsRestart;
+ },
+
+ /**
+ * Get a list of items that are currently "active" (turned on) of a specific
+ * type
+ * @param type
+ * The nsIUpdateItem type to return a list of items of
+ * @returns An array of active items of the specified type.
+ */
+ _getActiveItems: function EM__getActiveItems(type) {
+ var allItems = this.getItemList(type);
+ var activeItems = [];
+ var ds = this.datasource;
+ for (var i = 0; i < allItems.length; ++i) {
+ var item = allItems[i];
+
+ var installLocation = this.getInstallLocation(item.id);
+ // An entry with an invalid install location is not active.
+ if (!installLocation)
+ continue;
+ // An item entry is valid only if it is not disabled, not about to
+ // be disabled, and not about to be uninstalled.
+ if (installLocation.name in StartupCache.entries &&
+ item.id in StartupCache.entries[installLocation.name] &&
+ StartupCache.entries[installLocation.name][item.id]) {
+ var op = StartupCache.entries[installLocation.name][item.id].op;
+ if (op == OP_NEEDS_INSTALL || op == OP_NEEDS_UPGRADE ||
+ op == OP_NEEDS_UNINSTALL || op == OP_NEEDS_DISABLE)
+ continue;
+ }
+ // Suppress items that have been disabled by the user or the app.
+ if (ds.getItemProperty(item.id, "isDisabled") != "true")
+ activeItems.push({ id: item.id, version: item.version,
+ location: installLocation });
+ }
+
+ return activeItems;
+ },
+
+ /**
+ * Write the Extensions List
+ */
+ _updateExtensionsManifest: function EM__updateExtensionsManifest() {
+ // When an operation is performed that requires a component re-registration
+ // (extension enabled/disabled, installed, uninstalled), we must write the
+ // set of paths where extensions live so that the startup system can determine
+ // where additional components, preferences, chrome manifests etc live.
+ //
+ // To do this we obtain a list of active extensions and themes and write
+ // these to the extensions.ini file in the profile directory.
+ var validExtensions = this._getActiveItems(Ci.nsIUpdateItem.TYPE_ANY -
+ Ci.nsIUpdateItem.TYPE_THEME);
+ var validThemes = this._getActiveItems(Ci.nsIUpdateItem.TYPE_THEME);
+
+ var extensionsLocationsFile = FileUtils.getFile(KEY_PROFILEDIR,
+ [FILE_EXTENSION_MANIFEST]);
+ var fos = FileUtils.openSafeFileOutputStream(extensionsLocationsFile);
+
+ var enabledItems = [];
+ var extensionSectionHeader = "[ExtensionDirs]\r\n";
+ fos.write(extensionSectionHeader, extensionSectionHeader.length);
+ for (var i = 0; i < validExtensions.length; ++i) {
+ var e = validExtensions[i];
+ var itemLocation = e.location.getItemLocation(e.id).QueryInterface(Ci.nsILocalFile);
+ var descriptor = getAbsoluteDescriptor(itemLocation);
+ var line = "Extension" + i + "=" + descriptor + "\r\n";
+ fos.write(line, line.length);
+ enabledItems.push(e.id + ":" + e.version);
+ }
+
+ var themeSectionHeader = "[ThemeDirs]\r\n";
+ fos.write(themeSectionHeader, themeSectionHeader.length);
+ for (i = 0; i < validThemes.length; ++i) {
+ var e = validThemes[i];
+ var itemLocation = e.location.getItemLocation(e.id).QueryInterface(Ci.nsILocalFile);
+ var descriptor = getAbsoluteDescriptor(itemLocation);
+ var line = "Extension" + i + "=" + descriptor + "\r\n";
+ fos.write(line, line.length);
+ enabledItems.push(e.id + ":" + e.version);
+ }
+
+ FileUtils.closeSafeFileOutputStream(fos);
+
+ // Cache the enabled list for annotating the crash report subsequently
+ gPref.setCharPref(PREF_EM_ENABLED_ITEMS, enabledItems.join(","));
+ },
+
+ /**
+ * Say whether or not the Extension List has changed (and thus whether or not
+ * the system will have to restart the next time it is started).
+ * @param val
+ * true if the Extension List has changed, false otherwise.
+ * @returns |val|
+ */
+ set _extensionListChanged(val) {
+ // When an extension has an operation perform on it (e.g. install, upgrade,
+ // disable, etc.) we are responsible for writing this information to
+ // compatibility.ini, and nsAppRunner is responsible for checking this on
+ // restart. At some point it may make sense to be able to cancel a
+ // registration but for now we only create the file.
+ if (val) {
+ let XRE = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
+ XRE.invalidateCachesOnRestart();
+ }
+ return val;
+ },
+
+ /**
+ * Gathers data about an item specified by the supplied Install Manifest
+ * and determines whether or not it can be installed as-is. It makes this
+ * determination by validating the item's GUID, Version, and determining
+ * if it is compatible with this application.
+ * @param installManifest
+ * A nsIRDFDataSource representing the Install Manifest of the
+ * item to be installed.
+ * @return A JS Object with the following properties:
+ * "id" The GUID of the Item being installed.
+ * "version" The Version string of the Item being installed.
+ * "name" The Name of the Item being installed.
+ * "type" The nsIUpdateItem type of the Item being installed.
+ * "targetApps" An array of TargetApplication Info Objects
+ * with "id", "minVersion" and "maxVersion" properties,
+ * representing applications targeted by this item.
+ * "error" The result code:
+ * INSTALLERROR_SUCCESS
+ * no error, item can be installed
+ * INSTALLERROR_INVALID_GUID
+ * error, GUID is not well-formed
+ * INSTALLERROR_INVALID_VERSION
+ * error, Version is not well-formed
+ * INSTALLERROR_INCOMPATIBLE_VERSION
+ * error, item is not compatible with this version
+ * of the application.
+ * INSTALLERROR_INCOMPATIBLE_PLATFORM
+ * error, item is not compatible with the operating
+ * system or ABI the application was built for.
+ * INSTALLERROR_INSECURE_UPDATE
+ * error, item has no secure method of providing updates
+ * INSTALLERROR_BLOCKLISTED
+ * error, item is blocklisted
+ */
+ _getInstallData: function EM__getInstallData(installManifest) {
+ var installData = { id : "",
+ version : "",
+ name : "",
+ type : 0,
+ error : INSTALLERROR_SUCCESS,
+ targetApps : [],
+ updateURL : "",
+ updateKey : "",
+ currentApp : null };
+
+ // Fetch properties from the Install Manifest
+ installData.id = getManifestProperty(installManifest, "id");
+ installData.version = getManifestProperty(installManifest, "version");
+ installData.name = getManifestProperty(installManifest, "name");
+ installData.type = getAddonTypeFromInstallManifest(installManifest);
+ installData.updateURL= getManifestProperty(installManifest, "updateURL");
+ installData.updateKey= getManifestProperty(installManifest, "updateKey");
+
+ /**
+ * Reads a property off a Target Application resource
+ * @param resource
+ * The RDF Resource for a Target Application
+ * @param property
+ * The property (less EM_NS) to read
+ * @returns The string literal value of the property.
+ */
+ function readTAProperty(resource, property) {
+ return stringData(installManifest.GetTarget(resource, EM_R(property), true));
+ }
+
+ var targetApps = installManifest.GetTargets(gInstallManifestRoot,
+ EM_R("targetApplication"),
+ true);
+ while (targetApps.hasMoreElements()) {
+ var targetApp = targetApps.getNext();
+ if (targetApp instanceof Ci.nsIRDFResource) {
+ try {
+ var data = { id : readTAProperty(targetApp, "id"),
+ minVersion: readTAProperty(targetApp, "minVersion"),
+ maxVersion: readTAProperty(targetApp, "maxVersion") };
+ installData.targetApps.push(data);
+ if ((data.id == gApp.ID) ||
+ (data.id == TOOLKIT_ID) && !installData.currentApp)
+ installData.currentApp = data;
+ }
+ catch (e) {
+ continue;
+ }
+ }
+ }
+
+ // If the item specifies one or more target platforms, make sure our OS/ABI
+ // combination is in the list - otherwise, refuse to install the item.
+ var targetPlatforms = null;
+ try {
+ targetPlatforms = installManifest.GetTargets(gInstallManifestRoot,
+ EM_R("targetPlatform"),
+ true);
+ } catch(e) {
+ // No targetPlatform nodes, continue.
+ }