7230 lines
269 KiB
JavaScript
7230 lines
269 KiB
JavaScript
/* -*- 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 <[email protected]> (Google Inc.)
|
|
* Benjamin Smedberg <[email protected]>
|
|
* Jens Bannmann <[email protected]>
|
|
* Robert Strong <[email protected]>
|
|
*
|
|
* 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 nsIExtensionManager = Components.interfaces.nsIExtensionManager;
|
|
const nsIAddonUpdateCheckListener = Components.interfaces.nsIAddonUpdateCheckListener;
|
|
const nsIUpdateItem = Components.interfaces.nsIUpdateItem;
|
|
const nsILocalFile = Components.interfaces.nsILocalFile;
|
|
const nsILineInputStream = Components.interfaces.nsILineInputStream;
|
|
const nsIInstallLocation = Components.interfaces.nsIInstallLocation;
|
|
const nsIURL = Components.interfaces.nsIURL
|
|
// XXXrstrong calling hasMoreElements on a nsIDirectoryEnumerator after
|
|
// it has been removed will cause a crash on Mac OS X - bug 292823
|
|
const nsIDirectoryEnumerator = Components.interfaces.nsIDirectoryEnumerator;
|
|
|
|
const PREF_EM_APP_EXTENSIONS_VERSION = "app.extensions.version";
|
|
const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion";
|
|
const PREF_UPDATE_COUNT = "extensions.update.count";
|
|
const PREF_UPDATE_DEFAULT_URL = "extensions.update.url";
|
|
const PREF_EM_IGNOREMTIMECHANGES = "extensions.ignoreMTimeChanges";
|
|
const PREF_EM_DISABLEDOBSOLETE = "extensions.disabledObsolete";
|
|
const PREF_EM_LAST_SELECTED_SKIN = "extensions.lastSelectedSkin";
|
|
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_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
|
|
const PREF_EM_LOGGING_ENABLED = "extensions.logging.enabled";
|
|
const PREF_EM_UPDATE_INTERVAL = "extensions.update.interval";
|
|
const PREF_XPINSTALL_STATUS_DLG_SKIN = "xpinstall.dialog.progress.skin";
|
|
|
|
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_AUTOREG = ".autoreg";
|
|
const FILE_INSTALL_MANIFEST = "install.rdf";
|
|
const FILE_CONTENTS_MANIFEST = "contents.rdf";
|
|
const FILE_CHROME_MANIFEST = "chrome.manifest";
|
|
|
|
const UNKNOWN_XPCOM_ABI = "unknownABI";
|
|
|
|
const FILE_LOGFILE = "extensionmanager.log";
|
|
|
|
const FILE_DEFAULT_THEME_JAR = "classic.jar";
|
|
|
|
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 CATEGORY_INSTALL_LOCATIONS = "extension-install-locations";
|
|
|
|
const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
|
|
const PREFIX_NS_CHROME = "http://www.mozilla.org/rdf/chrome#";
|
|
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_FINALIZE_DIALOG = "chrome://mozapps/content/extensions/finalize.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 INSTALLERROR_SUCCESS = 0;
|
|
const INSTALLERROR_INVALID_VERSION = -1;
|
|
const INSTALLERROR_INVALID_GUID = -2;
|
|
const INSTALLERROR_INCOMPATIBLE_VERSION = -3;
|
|
const INSTALLERROR_PHONED_HOME = -4;
|
|
const INSTALLERROR_INCOMPATIBLE_PLATFORM = -5;
|
|
|
|
const MODE_WRONLY = 0x02;
|
|
const MODE_CREATE = 0x08;
|
|
const MODE_APPEND = 0x10;
|
|
const MODE_TRUNCATE = 0x20;
|
|
|
|
const PERMS_FILE = 0644;
|
|
const PERMS_DIRECTORY = 0755;
|
|
|
|
var gApp = null;
|
|
var gPref = null;
|
|
var gRDF = null;
|
|
var gOS = null;
|
|
var gXPCOMABI = null;
|
|
var gOSTarget = null;
|
|
var gConsole = null;
|
|
var gInstallManifestRoot = null;
|
|
var gVersionChecker = null;
|
|
var gLoggingEnabled = null;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* Creates a Version Checker object.
|
|
* @returns A handle to the global Version Checker service.
|
|
*/
|
|
function getVersionChecker() {
|
|
if (!gVersionChecker) {
|
|
gVersionChecker = Components.classes["@mozilla.org/xpcom/version-comparator;1"]
|
|
.getService(Components.interfaces.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(bundleURI) {
|
|
var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
|
|
.getService(Components.interfaces.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 CHROME_NS(property) {
|
|
return PREFIX_NS_CHROME + 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);
|
|
}
|
|
|
|
/**
|
|
* 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 = Components.classes["@mozilla.org/rdf/container;1"]
|
|
.createInstance(Components.interfaces.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, type) {
|
|
var item = Components.classes["@mozilla.org/updates/item;1"]
|
|
.createInstance(Components.interfaces.nsIUpdateItem);
|
|
item.init(id, version, locationKey, minVersion, maxVersion, name,
|
|
updateURL, updateHash, iconURL, updateRDF, type);
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
* Gets the specified directory at the speciifed hierarchy under a
|
|
* Directory Service key.
|
|
* @param key
|
|
* The Directory Service Key to start from
|
|
* @param pathArray
|
|
* An array of path components to locate beneath the directory
|
|
* specified by |key|
|
|
* @return nsIFile object for the location specified. If the directory
|
|
* requested does not exist, it is created, along with any
|
|
* parent directories that need to be created.
|
|
*/
|
|
function getDir(key, pathArray) {
|
|
return getDirInternal(key, pathArray, true);
|
|
}
|
|
|
|
/**
|
|
* Gets the specified directory at the speciifed hierarchy under a
|
|
* Directory Service key.
|
|
* @param key
|
|
* The Directory Service Key to start from
|
|
* @param pathArray
|
|
* An array of path components to locate beneath the directory
|
|
* specified by |key|
|
|
* @return nsIFile object for the location specified. If the directory
|
|
* requested does not exist, it is NOT created.
|
|
*/
|
|
function getDirNoCreate(key, pathArray) {
|
|
return getDirInternal(key, pathArray, false);
|
|
}
|
|
|
|
/**
|
|
* Gets the specified directory at the speciifed hierarchy under a
|
|
* Directory Service key.
|
|
* @param key
|
|
* The Directory Service Key to start from
|
|
* @param pathArray
|
|
* An array of path components to locate beneath the directory
|
|
* specified by |key|
|
|
* @param shouldCreate
|
|
* true if the directory hierarchy specified in |pathArray|
|
|
* should be created if it does not exist,
|
|
* false otherwise.
|
|
* @return nsIFile object for the location specified.
|
|
*/
|
|
function getDirInternal(key, pathArray, shouldCreate) {
|
|
var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]
|
|
.getService(Components.interfaces.nsIProperties);
|
|
var dir = fileLocator.get(key, nsILocalFile);
|
|
for (var i = 0; i < pathArray.length; ++i) {
|
|
dir.append(pathArray[i]);
|
|
if (shouldCreate && !dir.exists())
|
|
dir.create(nsILocalFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
|
|
}
|
|
dir.followLinks = false;
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
* Gets the file at the speciifed hierarchy under a Directory Service key.
|
|
* @param key
|
|
* The Directory Service Key to start from
|
|
* @param pathArray
|
|
* An array of path components to locate beneath the directory
|
|
* specified by |key|. The last item in this array must be the
|
|
* leaf name of a file.
|
|
* @return nsIFile object for the file specified. The file is NOT created
|
|
* if it does not exist, however all required directories along
|
|
* the way are.
|
|
*/
|
|
function getFile(key, pathArray) {
|
|
var file = getDir(key, pathArray.slice(0, -1));
|
|
file.append(pathArray[pathArray.length - 1]);
|
|
return file;
|
|
}
|
|
|
|
/**
|
|
* 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 = Components.classes["@mozilla.org/file/local;1"]
|
|
.createInstance(nsILocalFile);
|
|
|
|
var m = descriptor.match(/^(abs|rel)\%(.*)$/);
|
|
if (!m)
|
|
throw Components.results.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 nsIURL)
|
|
var extension = fileURL.fileExtension.toLowerCase();
|
|
return extension == "xpi" || extension == "jar";
|
|
}
|
|
|
|
/**
|
|
* Return the leaf name used by the extension system for staging an item.
|
|
* @param id
|
|
* The GUID of the item
|
|
* @param type
|
|
* The nsIUpdateItem type of the item
|
|
* @returns The leaf name of the staged file.
|
|
*/
|
|
function getStagedLeafName(id, type) {
|
|
if (type == nsIUpdateItem.TYPE_THEME)
|
|
return id + ".jar";
|
|
return id + ".xpi";
|
|
}
|
|
|
|
/**
|
|
* Opens a safe file output stream for writing.
|
|
* @param file
|
|
* The file to write to.
|
|
* @param modeFlags
|
|
* (optional) File open flags. Can be undefined.
|
|
* @returns nsIFileOutputStream to write to.
|
|
*/
|
|
function openSafeFileOutputStream(file, modeFlags) {
|
|
var fos = Components.classes["@mozilla.org/network/safe-file-output-stream;1"]
|
|
.createInstance(Components.interfaces.nsIFileOutputStream);
|
|
if (modeFlags === undefined)
|
|
modeFlags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE;
|
|
if (!file.exists())
|
|
file.create(nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
fos.init(file, modeFlags, PERMS_FILE, 0);
|
|
return fos;
|
|
}
|
|
|
|
/**
|
|
* Closes a safe file output stream.
|
|
* @param stream
|
|
* The stream to close.
|
|
*/
|
|
function closeSafeFileOutputStream(stream) {
|
|
if (stream instanceof Components.interfaces.nsISafeOutputStream)
|
|
stream.finish();
|
|
else
|
|
stream.close();
|
|
}
|
|
|
|
/**
|
|
* 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(Components.interfaces.nsIFile);
|
|
|
|
if (entry.isDirectory()) {
|
|
removeDirRecursive(entry);
|
|
}
|
|
else {
|
|
entry.permissions = PERMS_FILE;
|
|
entry.remove(false);
|
|
}
|
|
}
|
|
dir.permissions = PERMS_DIRECTORY;
|
|
dir.remove(true);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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 & nsIUpdateItem.TYPE_EXTENSION)
|
|
return PREFIX_EXTENSION;
|
|
else if (type & 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 = Components.classes["@mozilla.org/network/io-service;1"]
|
|
.getService(Components.interfaces.nsIIOService);
|
|
var fph = ioServ.getProtocolHandler("file")
|
|
.QueryInterface(Components.interfaces.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 = Components.classes["@mozilla.org/network/io-service;1"]
|
|
.getService(Components.interfaces.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 = Components.classes["@mozilla.org/network/io-service;1"]
|
|
.getService(Components.interfaces.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 Components.interfaces.nsIRDFLiteral)
|
|
return literalOrResource.Value;
|
|
if (literalOrResource instanceof Components.interfaces.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 Components.interfaces.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 typeArc = gRDF.GetResource(EM_NS("type"));
|
|
var type = intData(installManifest.GetTarget(gInstallManifestRoot, typeArc, true));
|
|
if (type !== undefined)
|
|
return 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 nsIUpdateItem.TYPE_THEME;
|
|
|
|
// If no type is provided, default to "Extension"
|
|
return 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);
|
|
var message;
|
|
var targetAppData = installData.currentApp;
|
|
if (!targetAppData) {
|
|
params = [installData.name, installData.version, BundleManager.appName];
|
|
message = extensionStrings.formatStringFromName("incompatibleMessageNoApp",
|
|
params, params.length);
|
|
}
|
|
else if (targetAppData.minVersion == targetAppData.maxVersion) {
|
|
// If the min target app version and the max target app version are the same, don't show
|
|
// a message like, "Foo is only compatible with Firefox versions 0.7 to 0.7", rather just
|
|
// show, "Foo is only compatible with Firefox 0.7"
|
|
params = [installData.name, installData.version, BundleManager.appName, gApp.version,
|
|
installData.name, installData.version, BundleManager.appName,
|
|
targetAppData.minVersion];
|
|
message = extensionStrings.formatStringFromName("incompatibleMsgSingleAppVersion",
|
|
params, params.length);
|
|
}
|
|
else {
|
|
params = [installData.name, installData.version, BundleManager.appName, gApp.version,
|
|
installData.name, installData.version, BundleManager.appName,
|
|
targetAppData.minVersion, targetAppData.maxVersion];
|
|
message = extensionStrings.formatStringFromName("incompatibleMsg", params, params.length);
|
|
}
|
|
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
|
.getService(Components.interfaces.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 = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
|
.getService(Components.interfaces.nsIPromptService);
|
|
ps.alert(null, title, message);
|
|
}
|
|
|
|
/**
|
|
* 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 = Components.classes["@mozilla.org/libjar/zip-reader;1"]
|
|
.createInstance(Components.interfaces.nsIZipReader);
|
|
zipReader.init(zipFile);
|
|
zipReader.open();
|
|
}
|
|
catch (e) {
|
|
zipReader.close();
|
|
throw e;
|
|
}
|
|
return zipReader;
|
|
}
|
|
|
|
/**
|
|
* 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 = getFile(KEY_TEMPDIR, [getRandomFileName(fileName)]);
|
|
try {
|
|
var zipReader = getZipReaderForFile(zipFile);
|
|
zipReader.getEntry(fileName);
|
|
zipReader.extract(fileName, file);
|
|
zipReader.close();
|
|
}
|
|
catch (e) {
|
|
if (!suppressErrors) {
|
|
showMessage("missingFileTitle", [], "missingFileMessage",
|
|
[BundleManager.appName, fileName]);
|
|
throw e;
|
|
}
|
|
}
|
|
return file;
|
|
}
|
|
|
|
/**
|
|
* Show a message to the user informing them they are installing an old non-EM
|
|
* style Theme, and that these are not supported.
|
|
* @param installManifest
|
|
* The Old-Style Contents Manifest datasource representing the theme.
|
|
*/
|
|
function showOldThemeError(contentsManifest) {
|
|
var extensionStrings = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
|
|
var params = [extensionStrings.GetStringFromName("theme")];
|
|
var title = extensionStrings.formatStringFromName("incompatibleTitle",
|
|
params, params.length);
|
|
var appVersion = extensionStrings.GetStringFromName("incompatibleOlder");
|
|
|
|
try {
|
|
var ctr = getContainer(contentsManifest,
|
|
gRDF.GetResource("urn:mozilla:skin:root"));
|
|
var elts = ctr.GetElements();
|
|
var nameArc = gRDF.GetResource(CHROME_NS("displayName"));
|
|
while (elts.hasMoreElements()) {
|
|
var elt = elts.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
themeName = stringData(contentsManifest.GetTarget(elt, nameArc, true));
|
|
if (themeName)
|
|
break;
|
|
}
|
|
}
|
|
catch (e) {
|
|
themeName = extensionStrings.GetStringFromName("incompatibleThemeName");
|
|
}
|
|
|
|
params = [themeName, "", BundleManager.appName, gApp.version, themeName, "",
|
|
BundleManager.appName, appVersion];
|
|
var message = extensionStrings.formatStringFromName("incompatibleMsgSingleAppVersion",
|
|
params, params.length);
|
|
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
|
.getService(Components.interfaces.nsIPromptService);
|
|
ps.alert(null, title, message);
|
|
}
|
|
|
|
/**
|
|
* 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 fileURL = getURLSpecFromFile(file);
|
|
var ds = gRDF.GetDataSourceBlocking(fileURL);
|
|
var arcs = ds.ArcLabelsOut(gInstallManifestRoot);
|
|
if (!arcs.hasMoreElements()) {
|
|
ds = null;
|
|
var uri = Components.classes["@mozilla.org/network/io-service;1"]
|
|
.getService(Components.interfaces.nsIIOService)
|
|
.newFileURI(file);
|
|
var url = uri.QueryInterface(nsIURL);
|
|
showMessage("malformedTitle", [], "malformedMessage",
|
|
[BundleManager.appName, url.fileName]);
|
|
}
|
|
return ds;
|
|
}
|
|
|
|
/**
|
|
* An enumeration of items in a JS array.
|
|
* @constructor
|
|
*/
|
|
function ArrayEnumerator(aItems) {
|
|
this._index = 0;
|
|
if (aItems) {
|
|
for (var i = 0; i < aItems.length; ++i) {
|
|
if (!aItems[i])
|
|
aItems.splice(i, 1);
|
|
}
|
|
}
|
|
this._contents = aItems;
|
|
}
|
|
|
|
ArrayEnumerator.prototype = {
|
|
_index: 0,
|
|
_contents: [],
|
|
|
|
hasMoreElements: function() {
|
|
return this._index < this._contents.length;
|
|
},
|
|
|
|
getNext: function() {
|
|
return this._contents[this._index++];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* An enumeration of files in a JS array.
|
|
* @param files
|
|
* The files to enumerate
|
|
* @constructor
|
|
*/
|
|
function FileEnumerator(files) {
|
|
this._index = 0;
|
|
if (files) {
|
|
for (var i = 0; i < files.length; ++i) {
|
|
if (!files[i])
|
|
files.splice(i, 1);
|
|
}
|
|
}
|
|
this._contents = files;
|
|
}
|
|
|
|
FileEnumerator.prototype = {
|
|
_index: 0,
|
|
_contents: [],
|
|
|
|
/**
|
|
* 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() {
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
this._name = name;
|
|
if (location.exists()) {
|
|
if (!location.isDirectory())
|
|
throw new Error("location must be a directoy!");
|
|
}
|
|
else {
|
|
try {
|
|
location.create(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;
|
|
}
|
|
DirectoryInstallLocation.prototype = {
|
|
_name : "",
|
|
_location : null,
|
|
_locationToIDMap: null,
|
|
_restricted : false,
|
|
_priority : 0,
|
|
_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(file) {
|
|
var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
|
|
.createInstance(Components.interfaces.nsIFileInputStream);
|
|
fis.init(file, -1, -1, false);
|
|
var line = { value: "" };
|
|
if (fis instanceof nsILineInputStream)
|
|
fis.readLine(line);
|
|
fis.close();
|
|
if (line.value) {
|
|
var linkedDirectory = Components.classes["@mozilla.org/file/local;1"]
|
|
.createInstance(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(nsIDirectoryEnumerator);
|
|
while (true) {
|
|
var entry = entries.nextFile;
|
|
if (!entry)
|
|
break;
|
|
entry instanceof 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(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;
|
|
|
|
var testFile = this.location;
|
|
testFile.append("Access Privileges Test");
|
|
try {
|
|
testFile.createUnique(nsILocalFile.DIRECTORY_TYPE, 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(id) {
|
|
var itemLocation = this.location;
|
|
itemLocation.append(id);
|
|
if (itemLocation.exists() && !itemLocation.isDirectory())
|
|
return this._readDirectoryFromFile(itemLocation);
|
|
if (!itemLocation.exists() && this.canAccess)
|
|
itemLocation.create(nsILocalFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
|
|
return itemLocation;
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
itemIsManagedIndependently: function(id) {
|
|
var itemLocation = this.location;
|
|
itemLocation.append(id);
|
|
return itemLocation.exists() && !itemLocation.isDirectory();
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
getItemFile: function(id, filePath) {
|
|
var itemLocation = this.getItemLocation(id).clone();
|
|
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(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 = 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(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(nsIDirectoryEnumerator);
|
|
while (entries.hasMoreElements()) {
|
|
var file = entries.nextFile;
|
|
if (!(file instanceof 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 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(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 nsIDirectoryEnumerator)
|
|
entries.close();
|
|
}
|
|
catch (e) {
|
|
LOG("DirectoryInstallLocation::removeFile: failed to remove staged " +
|
|
" directory = " + parent.path + ", exception = " + e + "\n");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* See nsISupports.idl
|
|
*/
|
|
QueryInterface: function (iid) {
|
|
if (!iid.equals(Components.interfaces.nsIInstallLocation) &&
|
|
!iid.equals(Components.interfaces.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
//@line 1237 "/cygdrive/c/mozilla/toolkit/mozapps/extensions/src/nsExtensionManager.js.in"
|
|
|
|
const nsIWindowsRegKey = Components.interfaces.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 = Components.classes["@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;
|
|
|
|
//@line 1299 "/cygdrive/c/mozilla/toolkit/mozapps/extensions/src/nsExtensionManager.js.in"
|
|
|
|
// 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(key) {
|
|
var count = key.valueCount;
|
|
for (var i = 0; i < count; ++i) {
|
|
var id = key.getValueName(i);
|
|
|
|
var dir = Components.classes["@mozilla.org/file/local;1"]
|
|
.createInstance(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(id) {
|
|
return this._IDToDirMap[id];
|
|
},
|
|
|
|
getIDForLocation: function(dir) {
|
|
return this._DirToIDMap[dir.path];
|
|
},
|
|
|
|
getItemFile: function(id, filePath) {
|
|
var itemLocation = this.getItemLocation(id).clone();
|
|
var parts = filePath.split("/");
|
|
for (var i = 0; i < parts.length; ++i)
|
|
itemLocation.append(parts[i]);
|
|
return itemLocation;
|
|
},
|
|
|
|
itemIsManagedIndependently: function(id) {
|
|
return true;
|
|
},
|
|
|
|
QueryInterface: function(iid) {
|
|
if (!iid.equals(Components.interfaces.nsIInstallLocation) &&
|
|
!iid.equals(Components.interfaces.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
//@line 1387 "/cygdrive/c/mozilla/toolkit/mozapps/extensions/src/nsExtensionManager.js.in"
|
|
|
|
/**
|
|
* An object which handles the installation of an Extension.
|
|
* @constructor
|
|
*/
|
|
function Installer(ds, id, installLocation, type) {
|
|
this._ds = ds;
|
|
this._id = id;
|
|
this._type = type;
|
|
this._installLocation = installLocation;
|
|
this._metadataFile = this._installLocation
|
|
.getItemFile(this._id, FILE_INSTALL_MANIFEST);
|
|
}
|
|
Installer.prototype = {
|
|
// Item metadata
|
|
_id: null,
|
|
_ds: null,
|
|
_installLocation: null,
|
|
_metadataDS: null,
|
|
|
|
/**
|
|
* Gets the Install Manifest datasource we are installing from.
|
|
*/
|
|
get metadataDS() {
|
|
if (!this._metadataDS) {
|
|
if (!this._metadataFile.exists())
|
|
return null;
|
|
this._metadataDS = getInstallManifest(this._metadataFile);
|
|
if (!this._metadataDS) {
|
|
LOG("Installer::install: metadata datasource for extension " +
|
|
this._id + " at " + this._metadataFile.path + " could not be loaded. " +
|
|
" Installation will not proceed.");
|
|
}
|
|
}
|
|
return this._metadataDS;
|
|
},
|
|
|
|
/**
|
|
* Installs the Extension
|
|
* @param file
|
|
* A XPI/JAR file to install from. If this is null or does not exist,
|
|
* the item is assumed to be an expanded directory, located at the GUID
|
|
* key in the supplied Install Location.
|
|
*/
|
|
installFromFile: function(file) {
|
|
// Move files from the staging dir into the extension's final home.
|
|
if (file && file.exists()) {
|
|
this._installExtensionFiles(file);
|
|
}
|
|
|
|
if (!this.metadataDS)
|
|
return;
|
|
|
|
// Upgrade old-style contents.rdf Chrome Manifests if necessary.
|
|
if (this._type == nsIUpdateItem.TYPE_THEME)
|
|
this.upgradeThemeChrome();
|
|
else
|
|
this.upgradeExtensionChrome();
|
|
|
|
// Add metadata for the extension to the global extension metadata set
|
|
this._ds.addItemMetadata(this._id, this.metadataDS, this._installLocation);
|
|
},
|
|
|
|
/**
|
|
* Safely extract the Extension's files into the target folder.
|
|
* @param file
|
|
* The XPI/JAR file to install from.
|
|
*/
|
|
_installExtensionFiles: function(file) {
|
|
var installer = this;
|
|
/**
|
|
* Callback for |safeInstallOperation| that performs file level installation
|
|
* steps for an Extension.
|
|
* @param extensionID
|
|
* The GUID of the Extension being installed.
|
|
* @param installLocation
|
|
* The Install Location where the Extension is being installed.
|
|
* @param xpiFile
|
|
* The source XPI file that contains the Extension.
|
|
*/
|
|
function extractExtensionFiles(extensionID, installLocation, xpiFile) {
|
|
// Create a logger to log install operations for uninstall. This must be
|
|
// created in the |safeInstallOperation| callback, since it creates a file
|
|
// in the target directory. If we do this outside of the callback, we may
|
|
// be clobbering a file we should not be.
|
|
var zipReader = getZipReaderForFile(xpiFile);
|
|
|
|
// create directories first
|
|
var entries = zipReader.findEntries("*/");
|
|
while (entries.hasMoreElements()) {
|
|
var entry = entries.getNext().QueryInterface(Components.interfaces.nsIZipEntry);
|
|
var target = installLocation.getItemFile(extensionID, entry.name);
|
|
if (!target.exists()) {
|
|
try {
|
|
target.create(nsILocalFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
|
|
}
|
|
catch (e) {
|
|
LOG("extractExtensionsFiles: failed to create target directory for extraction " +
|
|
" file = " + target.path + ", exception = " + e + "\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
entries = zipReader.findEntries("*");
|
|
while (entries.hasMoreElements()) {
|
|
entry = entries.getNext().QueryInterface(Components.interfaces.nsIZipEntry);
|
|
target = installLocation.getItemFile(extensionID, entry.name);
|
|
if (target.exists())
|
|
continue;
|
|
|
|
try {
|
|
target.create(nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
}
|
|
catch (e) {
|
|
LOG("extractExtensionsFiles: failed to create target file for extraction " +
|
|
" file = " + target.path + ", exception = " + e + "\n");
|
|
}
|
|
zipReader.extract(entry.name, target);
|
|
}
|
|
zipReader.close();
|
|
}
|
|
|
|
var installer = this;
|
|
/**
|
|
* Callback for |safeInstallOperation| that performs file level installation
|
|
* steps for a Theme.
|
|
* @param id
|
|
* The GUID of the Theme being installed.
|
|
* @param installLocation
|
|
* The Install Location where the Theme is being installed.
|
|
* @param jarFile
|
|
* The source JAR file that contains the Theme.
|
|
*/
|
|
function extractThemeFiles(id, installLocation, jarFile) {
|
|
var themeDirectory = installLocation.getItemLocation(id);
|
|
var zipReader = getZipReaderForFile(jarFile);
|
|
|
|
// The only critical file is the install.rdf and we would not have
|
|
// gotten this far without one.
|
|
var rootFiles = [FILE_INSTALL_MANIFEST, FILE_CHROME_MANIFEST,
|
|
"preview.png", "icon.png"];
|
|
for (var i = 0; i < rootFiles.length; ++i) {
|
|
try {
|
|
var entry = zipReader.getEntry(rootFiles[i]);
|
|
var target = installLocation.getItemFile(id, rootFiles[i]);
|
|
zipReader.extract(rootFiles[i], target);
|
|
}
|
|
catch (e) {
|
|
}
|
|
}
|
|
|
|
var manifestFile = installLocation.getItemFile(id, FILE_CHROME_MANIFEST);
|
|
// new theme structure requires a chrome.manifest file
|
|
if (manifestFile.exists()) {
|
|
var entries = zipReader.findEntries(DIR_CHROME + "/*");
|
|
while (entries.hasMoreElements()) {
|
|
entry = entries.getNext().QueryInterface(Components.interfaces.nsIZipEntry);
|
|
if (entry.name.substr(entry.name.length - 1, 1) == "/")
|
|
continue;
|
|
target = installLocation.getItemFile(id, entry.name);
|
|
try {
|
|
target.create(nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
}
|
|
catch (e) {
|
|
LOG("extractThemeFiles: failed to create target file for extraction " +
|
|
" file = " + target.path + ", exception = " + e + "\n");
|
|
}
|
|
zipReader.extract(entry.name, target);
|
|
}
|
|
zipReader.close();
|
|
}
|
|
else { // old theme structure requires only an install.rdf
|
|
try {
|
|
var entry = zipReader.getEntry(FILE_CONTENTS_MANIFEST);
|
|
var contentsManifestFile = installLocation.getItemFile(id, FILE_CONTENTS_MANIFEST);
|
|
contentsManifestFile.create(nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
zipReader.extract(FILE_CONTENTS_MANIFEST, contentsManifestFile);
|
|
}
|
|
catch (e) {
|
|
zipReader.close();
|
|
LOG("extractThemeFiles: failed to extract contents.rdf: " + target.path);
|
|
throw e; // let the safe-op clean up
|
|
}
|
|
zipReader.close();
|
|
var chromeDir = installLocation.getItemFile(id, DIR_CHROME);
|
|
try {
|
|
jarFile.copyTo(chromeDir, jarFile.fileName);
|
|
}
|
|
catch (e) {
|
|
LOG("extractThemeFiles: failed to copy theme JAR file to: " + chromeDir.path);
|
|
throw e; // let the safe-op clean up
|
|
}
|
|
|
|
if (!installer.metadataDS && installer._type == nsIUpdateItem.TYPE_THEME) {
|
|
if (contentsManifestFile && contentsManifestFile.exists()) {
|
|
var contentsManifest = gRDF.GetDataSourceBlocking(getURLSpecFromFile(contentsManifestFile));
|
|
showOldThemeError(contentsManifest);
|
|
}
|
|
LOG("Theme JAR file: " + jarFile.leafName + " contains an Old-Style " +
|
|
"Theme that is not compatible with this version of the software.");
|
|
throw new Error("Old Theme"); // let the safe-op clean up
|
|
}
|
|
}
|
|
}
|
|
|
|
var callback = extractExtensionFiles;
|
|
if (this._type == nsIUpdateItem.TYPE_THEME)
|
|
callback = extractThemeFiles;
|
|
safeInstallOperation(this._id, this._installLocation,
|
|
{ callback: callback, data: file });
|
|
},
|
|
|
|
/**
|
|
* Upgrade contents.rdf Chrome Manifests used by this Theme to the new
|
|
* chrome.manifest format if necessary.
|
|
*/
|
|
upgradeThemeChrome: function() {
|
|
// Use the Chrome Registry API to install the theme there
|
|
var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
|
|
.getService(Components.interfaces.nsIToolkitChromeRegistry);
|
|
var manifestFile = this._installLocation.getItemFile(this._id, FILE_CHROME_MANIFEST);
|
|
if (manifestFile.exists() ||
|
|
this._id == stripPrefix(RDFURI_DEFAULT_THEME, PREFIX_ITEM_URI))
|
|
return;
|
|
|
|
try {
|
|
// creates a chrome manifest for themes
|
|
var manifestURI = getURIFromFile(manifestFile);
|
|
var chromeDir = this._installLocation.getItemFile(this._id, DIR_CHROME);
|
|
// We're relying on the fact that there is only one JAR file
|
|
// in the "chrome" directory. This is a hack, but it works.
|
|
var entries = chromeDir.directoryEntries.QueryInterface(nsIDirectoryEnumerator);
|
|
var jarFile = entries.nextFile;
|
|
if (jarFile) {
|
|
var jarFileURI = getURIFromFile(jarFile);
|
|
var contentsURI = newURI("jar:" + jarFileURI.spec + "!/");
|
|
var contentsFile = this._installLocation.getItemFile(this._id, FILE_CONTENTS_MANIFEST);
|
|
var contentsFileURI = getURIFromFile(contentsFile.parent);
|
|
|
|
cr.processContentsManifest(contentsFileURI, manifestURI, contentsURI, false, true);
|
|
}
|
|
entries.close();
|
|
contentsFile.remove(false);
|
|
}
|
|
catch (e) {
|
|
// Failed to register chrome, for any number of reasons - non-existent
|
|
// contents.rdf file at the location specified, malformed contents.rdf,
|
|
// etc. Set the pending op to be OP_NEEDS_UNINSTALL so that the
|
|
// extension is uninstalled properly during the subsequent uninstall
|
|
// pass in |ExtensionManager::_finalizeOperations|
|
|
LOG("upgradeThemeChrome: failed for theme " + this._id + " - why " +
|
|
"not convert to the new chrome.manifest format while you're at it? " +
|
|
"Failure exception: " + e);
|
|
showMessage("malformedRegistrationTitle", [], "malformedRegistrationMessage",
|
|
[BundleManager.appName]);
|
|
|
|
var stageFile = this._installLocation.getStageFile(this._id);
|
|
if (stageFile)
|
|
this._installLocation.removeFile(stageFile);
|
|
|
|
StartupCache.put(this._installLocation, this._id, OP_NEEDS_UNINSTALL, true);
|
|
StartupCache.write();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Upgrade contents.rdf Chrome Manifests used by this Extension to the new
|
|
* chrome.manifest format if necessary.
|
|
*/
|
|
upgradeExtensionChrome: function() {
|
|
// If the extension is aware of the new flat chrome manifests and has
|
|
// included one, just use it instead of generating one from the
|
|
// install.rdf/contents.rdf data.
|
|
var manifestFile = this._installLocation.getItemFile(this._id, FILE_CHROME_MANIFEST);
|
|
if (manifestFile.exists())
|
|
return;
|
|
|
|
try {
|
|
// Enumerate the metadata datasource files collection and register chrome
|
|
// for each file, calling _registerChrome for each.
|
|
var chromeDir = this._installLocation.getItemFile(this._id, DIR_CHROME);
|
|
|
|
if (!manifestFile.parent.exists())
|
|
return;
|
|
|
|
// Even if an extension doesn't have any chrome, we generate an empty
|
|
// manifest file so that we don't try to upgrade from the "old-style"
|
|
// chrome manifests at every startup.
|
|
manifestFile.create(nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
|
|
var manifestURI = getURIFromFile(manifestFile);
|
|
var files = this.metadataDS.GetTargets(gInstallManifestRoot, EM_R("file"), true);
|
|
while (files.hasMoreElements()) {
|
|
var file = files.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
var chromeFile = chromeDir.clone();
|
|
var fileName = file.Value.substr("urn:mozilla:extension:file:".length, file.Value.length);
|
|
chromeFile.append(fileName);
|
|
|
|
var fileURLSpec = getURLSpecFromFile(chromeFile);
|
|
if (!chromeFile.isDirectory()) {
|
|
var zipReader = getZipReaderForFile(chromeFile);
|
|
fileURLSpec = "jar:" + fileURLSpec + "!/";
|
|
var contentsFile = this._installLocation.getItemFile(this._id, FILE_CONTENTS_MANIFEST);
|
|
contentsFile.create(nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
}
|
|
|
|
var providers = [EM_R("package"), EM_R("skin"), EM_R("locale")];
|
|
for (var i = 0; i < providers.length; ++i) {
|
|
var items = this.metadataDS.GetTargets(file, providers[i], true);
|
|
while (items.hasMoreElements()) {
|
|
var item = items.getNext().QueryInterface(Components.interfaces.nsIRDFLiteral);
|
|
var fileURI = newURI(fileURLSpec + item.Value);
|
|
// Extract the contents.rdf files instead of opening them inside of
|
|
// the jar. This prevents the jar from being cached by the zip
|
|
// reader which will keep the jar in use and prevent deletion.
|
|
if (zipReader) {
|
|
zipReader.extract(item.Value + FILE_CONTENTS_MANIFEST, contentsFile);
|
|
var contentsFileURI = getURIFromFile(contentsFile.parent);
|
|
}
|
|
else
|
|
contentsFileURI = fileURI;
|
|
|
|
var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
|
|
.getService(Components.interfaces.nsIToolkitChromeRegistry);
|
|
cr.processContentsManifest(contentsFileURI, manifestURI, fileURI, true, false);
|
|
}
|
|
}
|
|
if (zipReader) {
|
|
zipReader.close();
|
|
zipReader = null;
|
|
contentsFile.remove(false);
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
// Failed to register chrome, for any number of reasons - non-existent
|
|
// contents.rdf file at the location specified, malformed contents.rdf,
|
|
// etc. Set the pending op to be OP_NEEDS_UNINSTALL so that the
|
|
// extension is uninstalled properly during the subsequent uninstall
|
|
// pass in |ExtensionManager::_finalizeOperations|
|
|
LOG("upgradeExtensionChrome: failed for extension " + this._id + " - why " +
|
|
"not convert to the new chrome.manifest format while you're at it? " +
|
|
"Failure exception: " + e);
|
|
showMessage("malformedRegistrationTitle", [], "malformedRegistrationMessage",
|
|
[BundleManager.appName]);
|
|
|
|
var stageFile = this._installLocation.getStageFile(this._id);
|
|
if (stageFile)
|
|
this._installLocation.removeFile(stageFile);
|
|
|
|
StartupCache.put(this._installLocation, this._id, OP_NEEDS_UNINSTALL, true);
|
|
StartupCache.write();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Safely attempt to perform a caller-defined install operation for a given
|
|
* item ID. 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 installCallback
|
|
* A caller supplied JS object with the following properties:
|
|
* "data" A data parameter to be passed to the callback.
|
|
* "callback" A function to perform the install operation. This
|
|
* function is passed three parameters:
|
|
* 1. The GUID of the item being operated on.
|
|
* 2. The Install Location where the item is installed.
|
|
* 3. The "data" parameter on the installCallback object.
|
|
*/
|
|
function safeInstallOperation(itemID, installLocation, installCallback) {
|
|
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) {
|
|
LOG("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) {
|
|
LOG("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(nsIDirectoryEnumerator);
|
|
while (true) {
|
|
var entry = entries.nextFile;
|
|
if (!entry)
|
|
break;
|
|
if (entry.isDirectory())
|
|
moveDirectory(sourceDir, targetDir, entry);
|
|
else if (entry instanceof nsILocalFile) {
|
|
var rd = entry.getRelativeDescriptor(sourceDir);
|
|
var destination = targetDir.clone().QueryInterface(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) {
|
|
LOG("safeInstallOperation: failed to clean up the temporary backup of the " +
|
|
"older version: " + itemLocationTrash.path);
|
|
// This is a non-fatal error. Annoying, but non-fatal.
|
|
}
|
|
}
|
|
|
|
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) {
|
|
LOG("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) {
|
|
LOG("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;
|
|
}
|
|
}
|
|
|
|
// Now tell the client to do their stuff.
|
|
try {
|
|
installCallback.callback(itemID, installLocation, installCallback.data);
|
|
}
|
|
catch (e) {
|
|
// This means the install operation failed. Remove everything and roll back.
|
|
LOG("safeInstallOperation: install operation (caller-supplied callback) failed, " +
|
|
"rolling back file moves and aborting installation.");
|
|
try {
|
|
// Us-generated. Safe.
|
|
removeDirRecursive(itemLocation);
|
|
}
|
|
catch (e) {
|
|
LOG("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;
|
|
}
|
|
|
|
// 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(opType, entry) {
|
|
if (!(opType in this._ops))
|
|
this._ops[opType] = [];
|
|
this._ops[opType].push(entry);
|
|
},
|
|
|
|
/**
|
|
* 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(opType, id) {
|
|
if (!(opType in this._ops))
|
|
return;
|
|
for (var i = 0; i < this._ops[opType].length; ++i) {
|
|
if (this._ops[opType][i].id == id)
|
|
this._ops[opType].splice(i, 1);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove all Pending Operations of a certain type
|
|
* @param opType
|
|
* The type of Operation to remove all entries for
|
|
*/
|
|
clearItems: function(opType) {
|
|
if (opType in this._ops)
|
|
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(opType) {
|
|
return opType in this._ops ? this._ops[opType] : [];
|
|
},
|
|
|
|
/**
|
|
* The total number of Pending Operations, for all types.
|
|
*/
|
|
get size() {
|
|
var size = 0;
|
|
for (var opType in this._ops)
|
|
size += this._ops[opType].length;
|
|
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(name) {
|
|
return name in this._locations ? this._locations[name] : null;
|
|
},
|
|
|
|
/**
|
|
* Registers an Install Location
|
|
* @param installLocation
|
|
* The Install Location to register
|
|
*/
|
|
put: function(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(installLocation, id, op, shouldCreate) {
|
|
var itemLocation = installLocation.getItemLocation(id);
|
|
|
|
var descriptor = null;
|
|
var mtime = null;
|
|
if (itemLocation) {
|
|
itemLocation.QueryInterface(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(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(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(id) {
|
|
var entries = [];
|
|
for (var key in this.entries) {
|
|
if (id in this.entries[key])
|
|
entries.push(this.entries[key][id]);
|
|
}
|
|
return entries;
|
|
},
|
|
|
|
/**
|
|
* Call a function on each entry. The callback function takes a single
|
|
* parameter, which is an entry object.
|
|
*/
|
|
forEachEntry: function(callback) {
|
|
for (var key in this.entries) {
|
|
for (id in this.entries[key])
|
|
callback(this.entries[key][id]);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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() {
|
|
var itemChangeManifest = 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.
|
|
return;
|
|
}
|
|
var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
|
|
.createInstance(Components.interfaces.nsIFileInputStream);
|
|
fis.init(itemChangeManifest, -1, -1, false);
|
|
if (fis instanceof 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");
|
|
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() {
|
|
var extensionsCacheFile = getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS_STARTUP_CACHE]);
|
|
var fos = 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) {}
|
|
}
|
|
}
|
|
}
|
|
closeSafeFileOutputStream(fos);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Installs, manages and tracks compatibility for Extensions and Themes
|
|
* @constructor
|
|
*/
|
|
function ExtensionManager() {
|
|
gApp = Components.classes["@mozilla.org/xre/app-info;1"]
|
|
.getService(Components.interfaces.nsIXULAppInfo)
|
|
.QueryInterface(Components.interfaces.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 = Components.classes["@mozilla.org/preferences-service;1"]
|
|
.getService(Components.interfaces.nsIPrefBranch2);
|
|
gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
|
|
gPref.addObserver(PREF_EM_LOGGING_ENABLED, this, false);
|
|
|
|
gOS = Components.classes["@mozilla.org/observer-service;1"]
|
|
.getService(Components.interfaces.nsIObserverService);
|
|
gOS.addObserver(this, "xpcom-shutdown", false);
|
|
|
|
gConsole = Components.classes["@mozilla.org/consoleservice;1"]
|
|
.getService(Components.interfaces.nsIConsoleService);
|
|
|
|
gRDF = Components.classes["@mozilla.org/rdf/rdf-service;1"]
|
|
.getService(Components.interfaces.nsIRDFService);
|
|
gInstallManifestRoot = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT);
|
|
|
|
// Register Global Install Location
|
|
var appGlobalExtensions = getDirNoCreate(KEY_APPDIR, [DIR_EXTENSIONS]);
|
|
var priority = nsIInstallLocation.PRIORITY_APP_SYSTEM_GLOBAL;
|
|
var globalLocation = new DirectoryInstallLocation(KEY_APP_GLOBAL,
|
|
appGlobalExtensions, true,
|
|
priority);
|
|
InstallLocations.put(globalLocation);
|
|
|
|
// Register App-Profile Install Location
|
|
var appProfileExtensions = getDirNoCreate(KEY_PROFILEDS, [DIR_EXTENSIONS]);
|
|
var priority = nsIInstallLocation.PRIORITY_APP_PROFILE;
|
|
var profileLocation = new DirectoryInstallLocation(KEY_APP_PROFILE,
|
|
appProfileExtensions, false,
|
|
priority);
|
|
InstallLocations.put(profileLocation);
|
|
|
|
//@line 2280 "/cygdrive/c/mozilla/toolkit/mozapps/extensions/src/nsExtensionManager.js.in"
|
|
// Register HKEY_LOCAL_MACHINE Install Location
|
|
InstallLocations.put(
|
|
new WinRegInstallLocation("winreg-app-global",
|
|
nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
|
|
true,
|
|
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,
|
|
nsIInstallLocation.PRIORITY_APP_SYSTEM_USER + 10));
|
|
//@line 2294 "/cygdrive/c/mozilla/toolkit/mozapps/extensions/src/nsExtensionManager.js.in"
|
|
|
|
// Register Additional Install Locations
|
|
var categoryManager = Components.classes["@mozilla.org/categorymanager;1"]
|
|
.getService(Components.interfaces.nsICategoryManager);
|
|
var locations = categoryManager.enumerateCategory(CATEGORY_INSTALL_LOCATIONS);
|
|
while (locations.hasMoreElements()) {
|
|
var entry = locations.getNext().QueryInterface(Components.interfaces.nsISupportsCString).data;
|
|
var contractID = categoryManager.getCategoryEntry(CATEGORY_INSTALL_LOCATIONS, entry);
|
|
var location = Components.classes[contractID].getService(nsIInstallLocation);
|
|
InstallLocations.put(location);
|
|
}
|
|
}
|
|
|
|
ExtensionManager.prototype = {
|
|
/**
|
|
* See nsIObserver.idl
|
|
*/
|
|
observe: function(subject, topic, data) {
|
|
switch (topic) {
|
|
case "app-startup":
|
|
gOS.addObserver(this, "profile-after-change", false);
|
|
break;
|
|
case "profile-after-change":
|
|
this._profileSelected();
|
|
break;
|
|
case "quit-application-requested":
|
|
this._confirmCancelDownloadsOnQuit(subject);
|
|
break;
|
|
case "offline-requested":
|
|
this._confirmCancelDownloadsOnOffline(subject);
|
|
break;
|
|
case "xpcom-shutdown":
|
|
this._shutdown();
|
|
break;
|
|
case "nsPref:changed":
|
|
if (data == PREF_EM_LOGGING_ENABLED)
|
|
this._loggingToggled();
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Refresh the logging enabled global from preferences when the user changes
|
|
* the preference settting.
|
|
*/
|
|
_loggingToggled: function() {
|
|
gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
|
|
},
|
|
|
|
/**
|
|
* Initialize the system after a profile has been selected.
|
|
*/
|
|
_profileSelected: function() {
|
|
// 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);
|
|
}
|
|
}
|
|
catch (e) {
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clean up on application shutdown to avoid leaks.
|
|
*/
|
|
_shutdown: function() {
|
|
gOS.removeObserver(this, "xpcom-shutdown");
|
|
|
|
// Release strongly held services.
|
|
gOS = null;
|
|
if (this._ds && gRDF)
|
|
gRDF.UnregisterDataSource(this._ds)
|
|
gRDF = null;
|
|
if (gPref)
|
|
gPref.removeObserver(PREF_EM_LOGGING_ENABLED, 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 () {
|
|
var extensionsDS = getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS]);
|
|
var extensionsINI = getFile(KEY_PROFILEDIR, [FILE_EXTENSION_MANIFEST]);
|
|
var extensionsCache = getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS_STARTUP_CACHE]);
|
|
|
|
var dsExists = extensionsDS.exists();
|
|
var iniExists = extensionsINI.exists();
|
|
var cacheExists = extensionsCache.exists();
|
|
|
|
if (dsExists && iniExists && cacheExists)
|
|
return false;
|
|
|
|
// 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;
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
start: function(commandLine) {
|
|
var isDirty = false;
|
|
var forceAutoReg = false;
|
|
|
|
// Somehow the component list went away, and for that reason the new one
|
|
// generated by this function is going to result in a different compreg.
|
|
// We must force a restart.
|
|
var componentList = getFile(KEY_PROFILEDIR, [FILE_EXTENSION_MANIFEST]);
|
|
if (!componentList.exists())
|
|
forceAutoReg = true;
|
|
|
|
// 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.
|
|
if (this._ensureDatasetIntegrity())
|
|
isDirty = true;
|
|
|
|
// 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;
|
|
|
|
if (PendingOperations.size != 0)
|
|
isDirty = true;
|
|
|
|
// Extension Changes
|
|
if (isDirty) {
|
|
var needsRestart = this._finishOperations();
|
|
|
|
if (forceAutoReg) {
|
|
this._extensionListChanged = true;
|
|
needsRestart = true;
|
|
}
|
|
return needsRestart;
|
|
}
|
|
|
|
this._startTimers();
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Begins all background update check timers
|
|
*/
|
|
_startTimers: function() {
|
|
// Register a background update check timer
|
|
var tm =
|
|
Components.classes["@mozilla.org/updates/timer-manager;1"]
|
|
.getService(Components.interfaces.nsIUpdateTimerManager);
|
|
var interval = getPref("getIntPref", PREF_EM_UPDATE_INTERVAL, 86400);
|
|
tm.registerTimer("addon-background-update-timer", this, interval);
|
|
},
|
|
|
|
/**
|
|
* Notified when a timer fires
|
|
* @param timer
|
|
* The timer that fired
|
|
*/
|
|
notify: function(timer) {
|
|
if (getPref("getBoolPref", PREF_EM_UPDATE_ENABLED, true))
|
|
this.update([], 0, false, null);
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
handleCommandLineArgs: function(commandLine) {
|
|
try {
|
|
var globalExtension = commandLine.handleFlagWithParam("install-global-extension", false);
|
|
if (globalExtension) {
|
|
var file = commandLine.resolveFile(globalExtension);
|
|
this._installGlobalItem(file);
|
|
}
|
|
var globalTheme = commandLine.handleFlagWithParam("install-global-theme", false);
|
|
if (globalTheme) {
|
|
file = commandLine.resolveFile(globalTheme);
|
|
this._installGlobalItem(file);
|
|
}
|
|
}
|
|
catch (e) {
|
|
LOG("ExtensionManager:handleCommandLineArgs - failure, catching exception - lineno: " +
|
|
e.lineNumber + " - file: " + e.fileName + " - " + e);
|
|
}
|
|
commandLine.preventDefault = true;
|
|
},
|
|
|
|
/**
|
|
* Installs an XPI/JAR file into the KEY_APP_GLOBAL install location.
|
|
* @param file
|
|
* The XPI/JAR file to extract
|
|
*/
|
|
_installGlobalItem: function(file) {
|
|
if (!file || !file.exists())
|
|
throw new Error("Unable to find the file specified on the command line!");
|
|
var installManifestFile = extractRDFFileToTempDir(file, FILE_INSTALL_MANIFEST, true);
|
|
if (!installManifestFile.exists())
|
|
throw new Error("The package is missing an install manifest!");
|
|
var installManifest = getInstallManifest(installManifestFile);
|
|
installManifestFile.remove(false);
|
|
var installData = this._getInstallData(installManifest);
|
|
var installer = new Installer(installManifest, installData.id,
|
|
InstallLocations.get(KEY_APP_GLOBAL),
|
|
installData.type);
|
|
installer._installExtensionFiles(file);
|
|
if (installData.type == nsIUpdateItem.TYPE_EXTENSION)
|
|
installer.upgradeExtensionChrome();
|
|
else
|
|
installer.upgradeThemeChrome();
|
|
},
|
|
|
|
/**
|
|
* 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(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"),
|
|
installData.type);
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* 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() {
|
|
var em = this;
|
|
/**
|
|
* 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.
|
|
*/
|
|
function 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 = em._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);
|
|
em._appDisableItem(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_VERSION:
|
|
return "Incompatible 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 = Components.classes["@mozilla.org/embedcomp/dialogparam;1"]
|
|
.createInstance(Components.interfaces.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 = Components.classes["@mozilla.org/supports-string;1"]
|
|
.createInstance(Components.interfaces.nsISupportsString);
|
|
var bundle = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
|
|
supportsString.data = bundle.GetStringFromName("droppedInWarning");
|
|
var objs = Components.classes["@mozilla.org/array;1"]
|
|
.createInstance(Components.interfaces.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 = Components.classes["@mozilla.org/supports-interface-pointer;1"]
|
|
.createInstance(Components.interfaces.nsISupportsInterfacePointer);
|
|
ifptr.data = dpb;
|
|
ifptr.dataIID = Components.interfaces.nsIDialogParamBlock;
|
|
var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
|
|
.getService(Components.interfaces.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(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) {
|
|
droppedInFiles.push({ file: entry, location: location });
|
|
|
|
var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"]
|
|
.createInstance(Components.interfaces.nsIZipReader);
|
|
zipReader.init(entry);
|
|
var prettyName = "";
|
|
try {
|
|
var jar = zipReader.QueryInterface(Components.interfaces.nsIJAR);
|
|
var principal = { };
|
|
var certPrincipal = zipReader.getCertificatePrincipal(null, principal);
|
|
// XXXbz This string could be empty. This needs better
|
|
// UI to present principal.value.certificate's subject.
|
|
prettyName = principal.value.prettyName;
|
|
}
|
|
catch (e) { }
|
|
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)) {
|
|
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...");
|
|
installItem(id, location,
|
|
function(installManifest, id, location, type) {
|
|
em._configureForthcomingItem(installManifest, id, location,
|
|
type);
|
|
});
|
|
isDirty = true;
|
|
}
|
|
}
|
|
|
|
// Ask the user if they want to install the dropped items, for security
|
|
// purposes.
|
|
installDroppedInFiles(droppedInFiles, xpinstallStrings);
|
|
|
|
return isDirty;
|
|
},
|
|
|
|
/**
|
|
* Upgrades contents.rdf files to chrome.manifest files for any existing
|
|
* Extensions and Themes.
|
|
* @returns true if actions were performed that require a restart, false
|
|
* otherwise.
|
|
*/
|
|
_upgradeChrome: function() {
|
|
if (inSafeMode())
|
|
return false;
|
|
|
|
var checkForNewChrome = false;
|
|
var ds = this.datasource;
|
|
// If we have extensions that were installed before the new flat chrome
|
|
// manifests, and are still valid, we need to manually create the flat
|
|
// manifest files.
|
|
var extensions = this._getActiveItems(nsIUpdateItem.TYPE_EXTENSION);
|
|
for (var i = 0; i < extensions.length; ++i) {
|
|
var e = extensions[i];
|
|
var itemLocation = e.location.getItemLocation(e.id);
|
|
var manifest = itemLocation.clone();
|
|
manifest.append(FILE_CHROME_MANIFEST);
|
|
if (!manifest.exists()) {
|
|
var installRDF = itemLocation.clone();
|
|
installRDF.append(FILE_INSTALL_MANIFEST);
|
|
var installLocation = this.getInstallLocation(e.id);
|
|
if (installLocation && installRDF.exists()) {
|
|
var itemLocation = installLocation.getItemLocation(e.id);
|
|
if (itemLocation.exists() && itemLocation.isDirectory()) {
|
|
var installer = new Installer(ds, e.id, installLocation,
|
|
nsIUpdateItem.TYPE_EXTENSION);
|
|
installer.upgradeExtensionChrome();
|
|
}
|
|
}
|
|
else {
|
|
ds.removeItemMetadata(e.id);
|
|
ds.removeItemFromContainer(e.id);
|
|
}
|
|
|
|
checkForNewChrome = true;
|
|
}
|
|
}
|
|
|
|
var themes = this._getActiveItems(nsIUpdateItem.TYPE_THEME);
|
|
// If we have themes that were installed before the new flat chrome
|
|
// manifests, and are still valid, we need to manually create the flat
|
|
// manifest files.
|
|
for (i = 0; i < themes.length; ++i) {
|
|
var item = themes[i];
|
|
var itemLocation = item.location.getItemLocation(item.id);
|
|
var manifest = itemLocation.clone();
|
|
manifest.append(FILE_CHROME_MANIFEST);
|
|
if (manifest.exists() ||
|
|
item.id == stripPrefix(RDFURI_DEFAULT_THEME, PREFIX_ITEM_URI))
|
|
continue;
|
|
|
|
var entries;
|
|
try {
|
|
var manifestURI = getURIFromFile(manifest);
|
|
var chromeDir = itemLocation.clone();
|
|
chromeDir.append(DIR_CHROME);
|
|
|
|
if (!chromeDir.exists() || !chromeDir.isDirectory()) {
|
|
ds.removeItemMetadata(item.id);
|
|
ds.removeItemFromContainer(item.id);
|
|
continue;
|
|
}
|
|
|
|
// We're relying on the fact that there is only one JAR file
|
|
// in the "chrome" directory. This is a hack, but it works.
|
|
entries = chromeDir.directoryEntries.QueryInterface(nsIDirectoryEnumerator);
|
|
var jarFile = entries.nextFile;
|
|
if (jarFile) {
|
|
var jarFileURI = getURIFromFile(jarFile);
|
|
var contentsURI = newURI("jar:" + jarFileURI.spec + "!/");
|
|
|
|
// Use the Chrome Registry API to install the theme there
|
|
var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
|
|
.getService(Components.interfaces.nsIToolkitChromeRegistry);
|
|
cr.processContentsManifest(contentsURI, manifestURI, contentsURI, false, true);
|
|
}
|
|
entries.close();
|
|
}
|
|
catch (e) {
|
|
LOG("_upgradeChrome: failed to upgrade contents manifest for " +
|
|
"theme: " + item.id + ", exception: " + e + "... The theme will be " +
|
|
"disabled.");
|
|
this._appDisableItem(item.id);
|
|
}
|
|
finally {
|
|
try {
|
|
entries.close();
|
|
}
|
|
catch (e) {
|
|
}
|
|
}
|
|
checkForNewChrome = true;
|
|
}
|
|
return checkForNewChrome;
|
|
},
|
|
|
|
_checkForUncoveredItem: function(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() {
|
|
try {
|
|
// Stuff has changed, load the Extensions datasource in all its RDFey
|
|
// glory.
|
|
var ds = this.datasource;
|
|
var updatedTargetAppInfos = [];
|
|
|
|
var needsRestart = false;
|
|
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[0].id;
|
|
ds.setItemProperty(id, EM_R("userDisabled"), null);
|
|
ds.setItemProperty(id, EM_R("appDisabled"), null);
|
|
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;
|
|
// Only set the userDisabled property when the item's appDisabled
|
|
// property is not true since an item can't be disabled by the user
|
|
// when it is already disabled by the application.
|
|
if (ds.getItemProperty(id, "appDisabled") != "true")
|
|
ds.setItemProperty(id, EM_R("userDisabled"), EM_L("true"));
|
|
installLocation = this.getInstallLocation(id);
|
|
StartupCache.put(installLocation, id, OP_NONE, true);
|
|
PendingOperations.clearItem(OP_NEEDS_DISABLE, id);
|
|
}
|
|
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 oldLocation = this.getInstallLocation(id);
|
|
var newLocation = InstallLocations.get(items[i].locationKey);
|
|
if (newLocation.priority <= oldLocation.priority) {
|
|
// check if there is updated app compatibility info
|
|
var newTargetAppInfo = ds.getUpdatedTargetAppInfo(id);
|
|
if (newTargetAppInfo)
|
|
updatedTargetAppInfos.push(newTargetAppInfo);
|
|
this._finalizeUpgrade(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);
|
|
}
|
|
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;
|
|
}
|
|
PendingOperations.clearItems(OP_NEEDS_UNINSTALL);
|
|
}
|
|
while (PendingOperations.size > 0);
|
|
|
|
// Upgrade contents.rdf files to the new chrome.manifest format for
|
|
// existing Extensions and Themes
|
|
if (this._upgradeChrome()) {
|
|
var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
|
|
.getService(Components.interfaces.nsIChromeRegistry);
|
|
cr.checkForNewChrome();
|
|
}
|
|
|
|
// 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);
|
|
|
|
// If there is updated app compatibility info update the data sources.
|
|
for (i = 0; i < updatedTargetAppInfos.length; ++i) {
|
|
ds.updateTargetAppInfo(updatedTargetAppInfos[i].id,
|
|
updatedTargetAppInfos[i].minVersion,
|
|
updatedTargetAppInfos[i].maxVersion);
|
|
}
|
|
}
|
|
catch (e) {
|
|
LOG("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() {
|
|
// 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 = getPref("getCharPref", PREF_EM_APP_EXTENSIONS_VERSION,
|
|
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;
|
|
}
|
|
|
|
// Version mismatch, we have to load the extensions datasource and do
|
|
// version checking. Time hit here doesn't matter since this doesn't happen
|
|
// all that often.
|
|
var extensionsDS = getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS]);
|
|
if (!extensionsDS.exists())
|
|
this._upgradeFromV10();
|
|
|
|
// Make the extensions datasource consistent if it isn't already.
|
|
var isDirty = false;
|
|
if (this._ensureDatasetIntegrity())
|
|
isDirty = true;
|
|
|
|
if (this._checkForFileChanges())
|
|
isDirty = true;
|
|
|
|
if (PendingOperations.size != 0)
|
|
isDirty = true;
|
|
|
|
if (isDirty)
|
|
this._finishOperations();
|
|
|
|
var ds = this.datasource;
|
|
var allAppManaged = true;
|
|
var ctr = getContainer(ds, ds._itemRoot);
|
|
var elements = ctr.GetElements();
|
|
while (elements.hasMoreElements()) {
|
|
var itemResource = elements.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
var id = stripPrefix(itemResource.Value, PREFIX_ITEM_URI);
|
|
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.
|
|
var location = this.getInstallLocation(id);
|
|
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;
|
|
if (ds.getItemProperty(id, "compatible") == "true") {
|
|
if (ds.getItemProperty(id, "appDisabled"))
|
|
ds.setItemProperty(id, EM_R("appDisabled"), null);
|
|
}
|
|
else if (!ds.getItemProperty(id, "appDisabled"))
|
|
ds.setItemProperty(id, EM_R("appDisabled"), EM_L("true"));
|
|
}
|
|
this._updateManifests(true);
|
|
|
|
// Always check for compatibility updates when upgrading if we have add-ons
|
|
// that aren't managed by the application.
|
|
if (!allAppManaged)
|
|
this._showMismatchWindow();
|
|
|
|
// 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);
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Shows the "Compatibility Updates" UI
|
|
*/
|
|
_showMismatchWindow: function(items) {
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
var wizard = wm.getMostRecentWindow("Update:Wizard");
|
|
if (wizard)
|
|
wizard.focus();
|
|
else {
|
|
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 = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
|
|
.getService(Components.interfaces.nsIWindowWatcher);
|
|
ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, null);
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Catch all for facilitating a version 1.0 profile upgrade.
|
|
* 1) removes the abandoned default theme directory from the profile.
|
|
* 2) prepares themes installed with version 1.0 for installation.
|
|
* 3) initiates an install to populate the new extensions datasource.
|
|
* 4) migrates the disabled attribute from the old datasource.
|
|
* 5) migrates the app compatibility info from the old datasource.
|
|
*/
|
|
_upgradeFromV10: function() {
|
|
// return early if the version 1.0 extensions datasource file doesn't exist.
|
|
var oldExtensionsFile = getFile(KEY_PROFILEDIR, [DIR_EXTENSIONS, "Extensions.rdf"]);
|
|
if (!oldExtensionsFile.exists())
|
|
return;
|
|
|
|
// Version 1.0 profiles have a default theme directory in the profile's
|
|
// extensions directory that will be disabled due to having a maxVersion
|
|
// of 1.0 so we must remove it if it exists.
|
|
var profileDefaultTheme = getDirNoCreate(KEY_PROFILEDS, [DIR_EXTENSIONS,
|
|
stripPrefix(RDFURI_DEFAULT_THEME, PREFIX_ITEM_URI)]);
|
|
if (profileDefaultTheme && profileDefaultTheme.exists())
|
|
removeDirRecursive(profileDefaultTheme);
|
|
|
|
// Version 0.9 profiles may have DOMi 1.0 with just an install.rdf
|
|
var profileDOMi = getDirNoCreate(KEY_PROFILEDS, [DIR_EXTENSIONS,
|
|
"{641d8d09-7dda-4850-8228-ac0ab65e2ac9}"]);
|
|
if (profileDOMi && profileDOMi.exists())
|
|
removeDirRecursive(profileDOMi);
|
|
|
|
// Prepare themes for installation
|
|
// Only enumerate directories in the app-profile and app-global locations.
|
|
var locations = [KEY_APP_PROFILE, KEY_APP_GLOBAL];
|
|
for (var i = 0; i < locations.length; ++i) {
|
|
var location = InstallLocations.get(locations[i]);
|
|
if (!location.canAccess)
|
|
continue;
|
|
|
|
var entries = location.itemLocations;
|
|
var entry;
|
|
while ((entry = entries.nextFile)) {
|
|
var installRDF = entry.clone();
|
|
installRDF.append(FILE_INSTALL_MANIFEST);
|
|
|
|
var chromeDir = entry.clone();
|
|
chromeDir.append(DIR_CHROME);
|
|
|
|
// It must be a directory without an install.rdf and it must contain
|
|
// a chrome directory
|
|
if (!entry.isDirectory() || installRDF.exists() || !chromeDir.exists())
|
|
continue;
|
|
|
|
var chromeEntries = chromeDir.directoryEntries.QueryInterface(nsIDirectoryEnumerator);
|
|
if (!chromeEntries.hasMoreElements())
|
|
continue;
|
|
|
|
// We're relying on the fact that there is only one JAR file
|
|
// in the "chrome" directory. This is a hack, but it works.
|
|
var jarFile = chromeEntries.nextFile;
|
|
var id = location.getIDForLocation(entry);
|
|
|
|
try {
|
|
var zipReader = getZipReaderForFile(jarFile);
|
|
zipReader.extract(FILE_INSTALL_MANIFEST, installRDF);
|
|
|
|
var contentsManifestFile = location.getItemFile(id, FILE_CONTENTS_MANIFEST);
|
|
zipReader.extract(FILE_CONTENTS_MANIFEST, contentsManifestFile);
|
|
|
|
var rootFiles = ["preview.png", "icon.png"];
|
|
for (var i = 0; i < rootFiles.length; ++i) {
|
|
try {
|
|
var target = location.getItemFile(id, rootFiles[i]);
|
|
zipReader.extract(rootFiles[i], target);
|
|
}
|
|
catch (e) {
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
LOG("ExtensionManager:_upgradeFromV10 - failed to extract theme files\r\n" +
|
|
"Exception: " + e);
|
|
}
|
|
zipReader.close();
|
|
}
|
|
}
|
|
|
|
// When upgrading from a version 1.0 profile we need to populate the
|
|
// extensions datasource with all items before checking for incompatible
|
|
// items since the datasource hasn't been created yet.
|
|
var itemsToCheck = [];
|
|
if (this._checkForFileChanges()) {
|
|
// Create a list of all items that are to be installed so we can migrate
|
|
// these items's settings to the new datasource.
|
|
var items = PendingOperations.getOperations(OP_NEEDS_INSTALL);
|
|
for (i = items.length - 1; i >= 0; --i) {
|
|
if (items[i].locationKey == KEY_APP_PROFILE ||
|
|
items[i].locationKey == KEY_APP_GLOBAL)
|
|
itemsToCheck.push(items[i].id);
|
|
}
|
|
this._finishOperations();
|
|
}
|
|
|
|
// If there are no items to migrate settings for return early.
|
|
if (itemsToCheck.length == 0)
|
|
return;
|
|
|
|
var fileURL = getURLSpecFromFile(oldExtensionsFile);
|
|
var oldExtensionsDS = gRDF.GetDataSourceBlocking(fileURL);
|
|
var versionChecker = getVersionChecker();
|
|
var ds = this.datasource;
|
|
var currAppVersion = getPref("getCharPref", PREF_EM_APP_EXTENSIONS_VERSION,
|
|
gApp.version);
|
|
var currAppID = gApp.ID;
|
|
for (var i = 0; i < itemsToCheck.length; ++i) {
|
|
var item = ds.getItemForID(itemsToCheck[i]);
|
|
var oldPrefix = (item.type == nsIUpdateItem.TYPE_EXTENSION) ? PREFIX_EXTENSION : PREFIX_THEME;
|
|
var oldRes = gRDF.GetResource(oldPrefix + item.id);
|
|
// Disable the item if it was disabled in the version 1.0 extensions
|
|
// datasource.
|
|
if (oldExtensionsDS.GetTarget(oldRes, EM_R("disabled"), true))
|
|
ds.setItemProperty(item.id, EM_R("userDisabled"), EM_L("true"));
|
|
|
|
// app enable all items. If it is incompatible it will be app disabled
|
|
// later on.
|
|
ds.setItemProperty(item.id, EM_R("appDisabled"), null);
|
|
|
|
// if the item is already compatible don't attempt to migrate the
|
|
// item's compatibility info
|
|
var newRes = getResourceForID(itemsToCheck[i]);
|
|
if (ds.isCompatible(ds, newRes))
|
|
continue;
|
|
|
|
var updatedMinVersion = null;
|
|
var updatedMaxVersion = null;
|
|
var targetApps = oldExtensionsDS.GetTargets(oldRes, EM_R("targetApplication"), true);
|
|
while (targetApps.hasMoreElements()) {
|
|
var targetApp = targetApps.getNext();
|
|
if (targetApp instanceof Components.interfaces.nsIRDFResource) {
|
|
try {
|
|
var foundAppID = stringData(oldExtensionsDS.GetTarget(targetApp, EM_R("id"), true));
|
|
if (foundAppID != currAppID) // Different target application
|
|
continue;
|
|
|
|
updatedMinVersion = stringData(oldExtensionsDS.GetTarget(targetApp, EM_R("minVersion"), true));
|
|
updatedMaxVersion = stringData(oldExtensionsDS.GetTarget(targetApp, EM_R("maxVersion"), true));
|
|
|
|
// Only set the target app info if the extension's target app info
|
|
// in the version 1.0 extensions datasource makes it compatible
|
|
if (versionChecker.compare(currAppVersion, updatedMinVersion) >= 0 &&
|
|
versionChecker.compare(currAppVersion, updatedMaxVersion) <= 0)
|
|
ds.updateTargetAppInfo(item.id, updatedMinVersion, updatedMaxVersion);
|
|
|
|
break;
|
|
}
|
|
catch (e) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Write the Extensions List and the Startup Cache
|
|
* @param needsRestart
|
|
* true if the application needs to restart again, false otherwise.
|
|
*/
|
|
_updateManifests: function(needsRestart) {
|
|
// Write the Startup Cache (All Items, visible or not)
|
|
StartupCache.write();
|
|
// Write the Extensions Locations Manifest (Visible, enabled items)
|
|
this._updateExtensionsManifest(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(type) {
|
|
var allItems = this.getItemList(type, { });
|
|
var activeItems = [];
|
|
var ds = this.datasource;
|
|
for (var i = 0; i < allItems.length; ++i) {
|
|
var item = allItems[i];
|
|
|
|
// An item entry is valid only if it is not disabled, not about to
|
|
// be disabled, and not about to be uninstalled.
|
|
var installLocation = this.getInstallLocation(item.id);
|
|
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, "disabled") != "true")
|
|
activeItems.push({ id: item.id, location: installLocation });
|
|
}
|
|
|
|
return activeItems;
|
|
},
|
|
|
|
/**
|
|
* Write the Extensions List
|
|
* @param needsRestart
|
|
* true if the application needs to restart again, false otherwise.
|
|
*/
|
|
_updateExtensionsManifest: function(needsRestart) {
|
|
// 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(nsIUpdateItem.TYPE_EXTENSION);
|
|
var validThemes = this._getActiveItems(nsIUpdateItem.TYPE_THEME);
|
|
|
|
var extensionsLocationsFile = getFile(KEY_PROFILEDIR, [FILE_EXTENSION_MANIFEST]);
|
|
var fos = openSafeFileOutputStream(extensionsLocationsFile);
|
|
|
|
var extensionSectionHeader = "[ExtensionDirs]\r\n";
|
|
fos.write(extensionSectionHeader, extensionSectionHeader.length);
|
|
for (i = 0; i < validExtensions.length; ++i) {
|
|
var e = validExtensions[i];
|
|
var itemLocation = e.location.getItemLocation(e.id).QueryInterface(nsILocalFile);
|
|
var descriptor = getAbsoluteDescriptor(itemLocation);
|
|
var line = "Extension" + i + "=" + descriptor + "\r\n";
|
|
fos.write(line, line.length);
|
|
}
|
|
|
|
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(nsILocalFile);
|
|
var descriptor = getAbsoluteDescriptor(itemLocation);
|
|
var line = "Extension" + i + "=" + descriptor + "\r\n";
|
|
fos.write(line, line.length);
|
|
}
|
|
|
|
closeSafeFileOutputStream(fos);
|
|
|
|
// Now refresh the compatibility manifest.
|
|
this._extensionListChanged = needsRestart;
|
|
},
|
|
|
|
/**
|
|
* 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 creating the .autoreg file and
|
|
// nsAppRunner is responsible for removing it on restart. At some point it
|
|
// may make sense to be able to cancel a registration but for now we only
|
|
// create the file.
|
|
try {
|
|
var autoregFile = getFile(KEY_PROFILEDIR, [FILE_AUTOREG]);
|
|
if (val && !autoregFile.exists())
|
|
autoregFile.create(nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
}
|
|
catch (e) {
|
|
}
|
|
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.
|
|
*/
|
|
_getInstallData: function(installManifest) {
|
|
var installData = { id : "",
|
|
version : "",
|
|
name : "",
|
|
type : 0,
|
|
error : INSTALLERROR_SUCCESS,
|
|
targetApps : [],
|
|
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");
|
|
|
|
/**
|
|
* 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 Components.interfaces.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)
|
|
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.
|
|
}
|
|
if (targetPlatforms != null && targetPlatforms.hasMoreElements()) {
|
|
var foundMatchingOS = false;
|
|
var foundMatchingOSAndABI = false;
|
|
var requireABICompatibility = false;
|
|
while (targetPlatforms.hasMoreElements()) {
|
|
var targetPlatform = stringData(targetPlatforms.getNext());
|
|
var tokens = targetPlatform.split("_");
|
|
var os = tokens[0];
|
|
var abi = (tokens.length > 1) ? tokens[1] : null;
|
|
if (os == gOSTarget) {
|
|
foundMatchingOS = true;
|
|
// The presence of any ABI part after our OS means ABI is important.
|
|
if (abi != null) {
|
|
requireABICompatibility = true;
|
|
// If we don't know our ABI, we can't be compatible
|
|
if (abi == gXPCOMABI && abi != UNKNOWN_XPCOM_ABI) {
|
|
foundMatchingOSAndABI = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!foundMatchingOS || (requireABICompatibility && !foundMatchingOSAndABI)) {
|
|
installData.error = INSTALLERROR_INCOMPATIBLE_PLATFORM;
|
|
return installData;
|
|
}
|
|
}
|
|
|
|
// Validate the Item ID
|
|
if (!gIDTest.test(installData.id)) {
|
|
installData.error = INSTALLERROR_INVALID_GUID;
|
|
return installData;
|
|
}
|
|
|
|
// Check the target application range specified by the extension metadata.
|
|
if (!this.datasource.isCompatible(installManifest, gInstallManifestRoot, undefined))
|
|
installData.error = INSTALLERROR_INCOMPATIBLE_VERSION;
|
|
|
|
return installData;
|
|
},
|
|
|
|
/**
|
|
* Installs an item from a XPI/JAR file.
|
|
* This is the main entry point into the Install system from outside code
|
|
* (e.g. XPInstall).
|
|
* @param aXPIFile
|
|
* The file to install from.
|
|
* @param aInstallLocationKey
|
|
* The name of the Install Location where this item should be
|
|
* installed.
|
|
*/
|
|
installItemFromFile: function(xpiFile, installLocationKey) {
|
|
this.installItemFromFileInternal(xpiFile, installLocationKey, null);
|
|
},
|
|
|
|
/**
|
|
* Installs an item from a XPI/JAR file.
|
|
* @param aXPIFile
|
|
* The file to install from.
|
|
* @param aInstallLocationKey
|
|
* The name of the Install Location where this item should be
|
|
* installed.
|
|
* @param aInstallManifest
|
|
* An updated Install Manifest from the Version Update check.
|
|
* Can be null when invoked from callers other than the Version
|
|
* Update check.
|
|
*/
|
|
installItemFromFileInternal: function(aXPIFile, aInstallLocationKey, aInstallManifest) {
|
|
var em = this;
|
|
/**
|
|
* Gets the Install Location for an Item.
|
|
* @param itemID
|
|
* The GUID of the item to find an Install Location for.
|
|
* @return An object implementing nsIInstallLocation which represents the
|
|
* location where the specified item should be installed.
|
|
* This can be:
|
|
* 1. an object that corresponds to the location key supplied to
|
|
* |installItemFromFileInternal|,
|
|
* 2. the default install location (the App Profile Extensions Folder)
|
|
* if no location key was supplied, or the location key supplied
|
|
* was not in the set of registered locations
|
|
* 3. null, if the location selected by 1 or 2 above does not support
|
|
* installs from XPI/JAR files, or that location is not writable
|
|
* with the current access privileges.
|
|
*/
|
|
function getInstallLocation(itemID) {
|
|
// Here I use "upgrade" to mean "install a different version of an item".
|
|
var installLocation = em.getInstallLocation(itemID);
|
|
if (!installLocation) {
|
|
// This is not an "upgrade", since we don't have any location data for the
|
|
// extension ID specified - that is, it's not in our database.
|
|
|
|
// Caller supplied a key to a registered location, use that location
|
|
// for the installation
|
|
installLocation = InstallLocations.get(aInstallLocationKey);
|
|
if (installLocation) {
|
|
// If the specified location does not have a common metadata location
|
|
// (e.g. extensions have no common root, or other location specified
|
|
// by the location implementation) - e.g. for a Registry Key enumeration
|
|
// location - we cannot install or upgrade using a XPI file, probably
|
|
// because these application types will be handling upgrading themselves.
|
|
// Just bail.
|
|
if (!installLocation.location) {
|
|
LOG("Install Location \"" + installLocation.name + "\" does not support " +
|
|
"installation of items from XPI/JAR files. You must manage " +
|
|
"installation and update of these items yourself.");
|
|
installLocation = null;
|
|
}
|
|
}
|
|
else {
|
|
// In the absence of a preferred install location, just default to
|
|
// the App-Profile
|
|
installLocation = InstallLocations.get(KEY_APP_PROFILE);
|
|
}
|
|
}
|
|
else {
|
|
// This is an "upgrade", but not through the Update System, because the
|
|
// Update code will not let an extension with an incompatible target
|
|
// app version range through to this point. This is an "upgrade" in the
|
|
// sense that the user found a different version of an installed extension
|
|
// and installed it through the web interface, so we have metadata.
|
|
|
|
// If the location is different, return the preferred location rather than
|
|
// the location of the currently installed version, because we may be in
|
|
// the situation where an item is being installed into the global app
|
|
// dir when there's a version in the profile dir.
|
|
if (installLocation.name != aInstallLocationKey)
|
|
installLocation = InstallLocations.get(aInstallLocationKey);
|
|
}
|
|
if (!installLocation.canAccess) {
|
|
LOG("Install Location\"" + installLocation.name + "\" cannot be written " +
|
|
"to with your access privileges. Installation will not proceed.");
|
|
installLocation = null;
|
|
}
|
|
return installLocation;
|
|
}
|
|
|
|
/**
|
|
* Stages a XPI file in the default item location specified by other
|
|
* applications when they registered with XulRunner if the item's
|
|
* install manifest specified compatibility with them.
|
|
*/
|
|
function stageXPIForOtherApps(xpiFile, installData) {
|
|
for (var i = 0; i < installData.targetApps.length; ++i) {
|
|
var targetApp = installData.targetApps[i];
|
|
if (targetApp.id != gApp.ID) {
|
|
/* XXXben uncomment when this works!
|
|
var settingsThingy = Components.classes[]
|
|
.getService(Components.interfaces.nsIXULRunnerSettingsThingy);
|
|
try {
|
|
var appPrefix = "SOFTWARE\\Mozilla\\XULRunner\\Applications\\";
|
|
var branch = settingsThingy.getBranch(appPrefix + targetApp.id);
|
|
var path = branch.getProperty("ExtensionsLocation");
|
|
var destination = Components.classes["@mozilla.org/file/local;1"]
|
|
.createInstance(nsILocalFile);
|
|
destination.initWithPath(path);
|
|
xpiFile.copyTo(file, xpiFile.leafName);
|
|
}
|
|
catch (e) {
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts and then starts the install for extensions / themes contained
|
|
* within a xpi.
|
|
*/
|
|
function installMultiXPI(xpiFile, installData) {
|
|
var fileURL = getURIFromFile(xpiFile).QueryInterface(nsIURL);
|
|
if (fileURL.fileExtension.toLowerCase() != "xpi") {
|
|
LOG("Invalid File Extension: Item: \"" + fileURL.fileName + "\" has an " +
|
|
"invalid file extension. Only xpi file extensions are allowed for " +
|
|
"multiple item packages.");
|
|
var bundle = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
|
|
showMessage("invalidFileExtTitle", [],
|
|
"invalidFileExtMessage", [installData.name,
|
|
fileURL.fileExtension,
|
|
bundle.GetStringFromName("type-" + installData.type)]);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var zipReader = getZipReaderForFile(xpiFile);
|
|
}
|
|
catch (e) {
|
|
LOG("installMultiXPI: failed to open xpi file: " + xpiFile.path);
|
|
throw e;
|
|
}
|
|
|
|
var searchForEntries = ["*.xpi", "*.jar"];
|
|
var files = [];
|
|
for (var i = 0; i < searchForEntries.length; ++i) {
|
|
var entries = zipReader.findEntries(searchForEntries[i]);
|
|
while (entries.hasMoreElements()) {
|
|
var entry = entries.getNext().QueryInterface(Components.interfaces.nsIZipEntry);
|
|
var target = getFile(KEY_TEMPDIR, [entry.name]);
|
|
try {
|
|
target.createUnique(nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
}
|
|
catch (e) {
|
|
LOG("installMultiXPI: failed to create target file for extraction " +
|
|
" file = " + target.path + ", exception = " + e + "\n");
|
|
}
|
|
zipReader.extract(entry.name, target);
|
|
files.push(target);
|
|
}
|
|
}
|
|
zipReader.close();
|
|
|
|
if (files.length == 0) {
|
|
LOG("Multiple Item Package: Item: \"" + fileURL.fileName + "\" does " +
|
|
"not contain a valid package to install.");
|
|
var bundle = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
|
|
showMessage("missingPackageFilesTitle",
|
|
[bundle.GetStringFromName("type-" + installData.type)],
|
|
"missingPackageFilesMessage", [installData.name,
|
|
bundle.GetStringFromName("type-" + installData.type)]);
|
|
return;
|
|
}
|
|
|
|
// When installing themes we need to open the theme manager if it is not
|
|
// already opened except when we are installing a xpi dropped into a
|
|
// extensions directory.
|
|
fileURL = getURIFromFile(files[files.length - 1]).QueryInterface(nsIURL);
|
|
if (fileURL.fileExtension.toLowerCase() == "jar") {
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
var win = wm.getMostRecentWindow("Extension:Manager-extensions");
|
|
if (win) {
|
|
win = wm.getMostRecentWindow("Extension:Manager-themes");
|
|
if (win) {
|
|
win.focus();
|
|
}
|
|
else {
|
|
var themeManagerURL = gPref.getCharPref(PREF_XPINSTALL_STATUS_DLG_SKIN);
|
|
var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
|
|
.getService(Components.interfaces.nsIWindowWatcher);
|
|
ww.openWindow(null, themeManagerURL,
|
|
"", "chrome,centerscreen,titlebar,dialog=no,resizable", []);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < files.length; ++i) {
|
|
em.installItemFromFileInternal(files[i], aInstallLocationKey, null);
|
|
files[i].remove(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An observer for the Extension Update System.
|
|
* @constructor
|
|
*/
|
|
function IncompatibleObserver() {}
|
|
IncompatibleObserver.prototype = {
|
|
_id: null,
|
|
_type: nsIUpdateItem.TYPE_ANY,
|
|
_xpi: null,
|
|
_installManifest: null,
|
|
_installRDF: null,
|
|
|
|
/**
|
|
* Ask the Extension Update System if there are any version updates for
|
|
* this item that will allow it to be compatible with this version of
|
|
* the Application.
|
|
* @param installManifest
|
|
* The Install Manifest datasource for the item.
|
|
* @param installData
|
|
* The Install Data object for the item.
|
|
* @param xpiFile
|
|
* The staged source XPI file that contains the item. Cleaned
|
|
* up by this process.
|
|
*/
|
|
checkForUpdates: function(installManifest, installData, xpiFile, installRDF) {
|
|
this._id = installData.id;
|
|
this._type = installData.type;
|
|
this._xpi = xpiFile;
|
|
this._installManifest = installManifest;
|
|
this._installRDF = installRDF;
|
|
|
|
var item = makeItem(installData.id, installData.version,
|
|
aInstallLocationKey,
|
|
installData.currentApp.minVersion,
|
|
installData.currentApp.maxVersion,
|
|
installData.name,
|
|
"", /* XPI Update URL */
|
|
"", /* XPI Update Hash */
|
|
"", /* Icon URL */
|
|
installData.updateURL || "",
|
|
installData.type);
|
|
em.update([item], 1, true, this);
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
onUpdateStarted: function() {
|
|
LOG("Phone Home Listener: Update Started");
|
|
em.datasource.onUpdateStarted();
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
onUpdateEnded: function() {
|
|
LOG("Phone Home Listener: Update Ended");
|
|
// We are responsible for cleaning up this file!
|
|
this._installRDF.remove(false);
|
|
em.datasource.onUpdateEnded();
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
onAddonUpdateStarted: function(addon) {
|
|
LOG("Phone Home Listener: Update For " + addon.id + " started");
|
|
em.datasource.addIncompatibleUpdateItem(addon.name, this._xpi.path,
|
|
addon.type, addon.version);
|
|
em.datasource.onAddonUpdateStarted(addon);
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
onAddonUpdateEnded: function(addon, status) {
|
|
LOG("Phone Home Listener: Update For " + addon.id + " ended, status = " + status);
|
|
em.datasource.removeDownload(this._xpi.path);
|
|
LOG("Version Check Phone Home Completed");
|
|
// Only compatibility updates (e.g. STATUS_VERSIONINFO) are currently
|
|
// supported
|
|
if (status == nsIAddonUpdateCheckListener.STATUS_VERSIONINFO) {
|
|
em.datasource.setTargetApplicationInfo(addon.id,
|
|
addon.minAppVersion,
|
|
addon.maxAppVersion,
|
|
this._installManifest);
|
|
|
|
// Try and install again, but use the updated compatibility DB
|
|
em.installItemFromFileInternal(this._xpi, aInstallLocationKey,
|
|
this._installManifest);
|
|
|
|
// Add the updated compatibility info to the datasource if done
|
|
if (StartupCache.entries[aInstallLocationKey][addon.id].op == OP_NONE) {
|
|
em.datasource.updateTargetAppInfo(addon.id, addon.minAppVersion,
|
|
addon.maxAppVersion);
|
|
}
|
|
else { // needs a restart
|
|
// Add updatedMinVersion and updatedMaxVersion so it can be used
|
|
// to update the data sources during the installation or upgrade.
|
|
em.datasource.setUpdatedTargetAppInfo(addon.id, addon.minAppVersion,
|
|
addon.maxAppVersion);
|
|
}
|
|
// Prevent the datasource file from being lazily recreated after
|
|
// it is deleted by calling Flush.
|
|
this._installManifest.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource);
|
|
this._installManifest.Flush();
|
|
}
|
|
else {
|
|
em.datasource.removeDownload(this._xpi.path);
|
|
showIncompatibleError(installData);
|
|
// We are responsible for cleaning up this file!
|
|
InstallLocations.get(aInstallLocationKey).removeFile(this._xpi);
|
|
}
|
|
em.datasource.onAddonUpdateEnded(addon, status);
|
|
},
|
|
|
|
/**
|
|
* See nsISupports.idl
|
|
*/
|
|
QueryInterface: function(iid) {
|
|
if (!iid.equals(Components.interfaces.nsIAddonUpdateCheckListener) &&
|
|
!iid.equals(Components.interfaces.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
}
|
|
}
|
|
|
|
var installManifestFile = extractRDFFileToTempDir(aXPIFile, FILE_INSTALL_MANIFEST, true);
|
|
var shouldPhoneHomeIfNecessary = false;
|
|
if (!aInstallManifest) {
|
|
// If we were not called with an Install Manifest, we were called from
|
|
// some other path than the Phone Home system, so we do want to phone
|
|
// home if the version is incompatible.
|
|
shouldPhoneHomeIfNecessary = true;
|
|
var installManifest = getInstallManifest(installManifestFile);
|
|
if (!installManifest) {
|
|
LOG("The Install Manifest supplied by this item is not well-formed. " +
|
|
"Installation will not proceed.");
|
|
installManifestFile.remove(false);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
installManifest = aInstallManifest;
|
|
|
|
var installData = this._getInstallData(installManifest);
|
|
switch (installData.error) {
|
|
case INSTALLERROR_INCOMPATIBLE_VERSION:
|
|
// Since the caller cleans up |aXPIFile|, and we're not yet sure whether or
|
|
// not we need it (we may need it if a remote version bump that makes it
|
|
// compatible is discovered by the call home) - so we must stage it for
|
|
// later ourselves.
|
|
if (shouldPhoneHomeIfNecessary && installData.currentApp) {
|
|
var installLocation = getInstallLocation(installData.id, aInstallLocationKey);
|
|
if (!installLocation) {
|
|
installManifestFile.remove(false);
|
|
return;
|
|
}
|
|
var stagedFile = installLocation.stageFile(aXPIFile, installData.id);
|
|
(new IncompatibleObserver(this)).checkForUpdates(installManifest,
|
|
installData, stagedFile,
|
|
installManifestFile);
|
|
// Return early to prevent deletion of the install manifest file.
|
|
return;
|
|
}
|
|
else {
|
|
// XXXben Look up XULRunnerSettingsThingy to see if there is a registered
|
|
// app that can handle this item, if so just stage and don't show
|
|
// this error!
|
|
showIncompatibleError(installData);
|
|
}
|
|
break;
|
|
case INSTALLERROR_SUCCESS:
|
|
// Installation of multiple extensions / themes contained within a single xpi.
|
|
if (installData.type == nsIUpdateItem.TYPE_MULTI_XPI) {
|
|
installMultiXPI(aXPIFile, installData);
|
|
break;
|
|
}
|
|
|
|
// Stage the extension's XPI so it can be extracted at the next restart.
|
|
var installLocation = getInstallLocation(installData.id, aInstallLocationKey);
|
|
if (!installLocation) {
|
|
// No cleanup of any of the staged XPI files should be required here,
|
|
// because this should only ever fail on the first recurse through
|
|
// this function, BEFORE staging takes place... technically speaking
|
|
// a location could become readonly during the phone home process,
|
|
// but that's an edge case I don't care about.
|
|
installManifestFile.remove(false);
|
|
return;
|
|
}
|
|
|
|
// Stage a copy of the XPI/JAR file for our own evil purposes...
|
|
stagedFile = installLocation.stageFile(aXPIFile, installData.id);
|
|
|
|
var restartRequired = this.installRequiresRestart(installData.id,
|
|
installData.type);
|
|
// Determine which configuration function to use based on whether or not
|
|
// there is data about this item in our datasource already - if there is
|
|
// we want to upgrade, otherwise we install fresh.
|
|
var ds = this.datasource;
|
|
if (installData.id in ds.visibleItems && ds.visibleItems[installData.id]) {
|
|
// We enter this function if any data corresponding to an existing GUID
|
|
// is found, regardless of its Install Location. We need to check before
|
|
// "upgrading" an item that Install Location of the new item is of equal
|
|
// or higher priority than the old item, to make sure the datasource only
|
|
// ever tracks metadata for active items.
|
|
var oldInstallLocation = this.getInstallLocation(installData.id);
|
|
if (oldInstallLocation.priority >= installLocation.priority) {
|
|
this._upgradeItem(installManifest, installData.id, installLocation,
|
|
installData.type);
|
|
if (!restartRequired) {
|
|
this._finalizeUpgrade(installData.id);
|
|
this._finalizeInstall(installData.id, stagedFile);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
this._configureForthcomingItem(installManifest, installData.id,
|
|
installLocation, installData.type);
|
|
if (!restartRequired)
|
|
this._finalizeInstall(installData.id, stagedFile);
|
|
}
|
|
this._updateManifests(restartRequired);
|
|
break;
|
|
case INSTALLERROR_INVALID_GUID:
|
|
LOG("Invalid GUID: Item has GUID: \"" + installData.id + "\"" +
|
|
" which is not well-formed.");
|
|
var bundle = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
|
|
showMessage("incompatibleTitle",
|
|
[bundle.GetStringFromName("type-" + installData.type)],
|
|
"invalidGUIDMessage", [installData.name, installData.id]);
|
|
break;
|
|
case INSTALLERROR_INVALID_VERSION:
|
|
LOG("Invalid Version: Item: \"" + installData.id + "\" has version " +
|
|
installData.version + " which is not well-formed.");
|
|
var bundle = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
|
|
showMessage("incompatibleTitle",
|
|
[bundle.GetStringFromName("type-" + installData.type)],
|
|
"invalidVersionMessage", [installData.name, installData.version]);
|
|
break;
|
|
case INSTALLERROR_INCOMPATIBLE_PLATFORM:
|
|
const osABI = gOSTarget + "_" + gXPCOMABI;
|
|
LOG("Incompatible Platform: Item: \"" + installData.id + "\" is not " +
|
|
"compatible with '" + osABI + "'.");
|
|
var bundle = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
|
|
showMessage("incompatibleTitle",
|
|
[bundle.GetStringFromName("type-" + installData.type)],
|
|
"incompatiblePlatformMessage",
|
|
[installData.name, BundleManager.appName, osABI]);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Check to see if this item supports other applications and in that case
|
|
// stage the the XPI file in the location specified by those applications.
|
|
stageXPIForOtherApps(aXPIFile, installData);
|
|
|
|
installManifestFile.remove(false);
|
|
},
|
|
|
|
/**
|
|
* Whether or not this type's installation/uninstallation requires
|
|
* the application to be restarted.
|
|
* @param id
|
|
* The GUID of the item
|
|
* @param type
|
|
* The nsIUpdateItem type of the item
|
|
* @returns true if installation of an item of this type requires a
|
|
* restart.
|
|
*/
|
|
installRequiresRestart: function(id, type) {
|
|
switch (type) {
|
|
case nsIUpdateItem.TYPE_EXTENSION:
|
|
return true;
|
|
case nsIUpdateItem.TYPE_THEME:
|
|
var internalName = this.datasource.getItemProperty(id, "internalName");
|
|
var needsRestart = false;
|
|
if (gPref.prefHasUserValue(PREF_DSS_SKIN_TO_SELECT))
|
|
needsRestart = internalName == gPref.getCharPref(PREF_DSS_SKIN_TO_SELECT);
|
|
if (!needsRestart &&
|
|
gPref.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN))
|
|
needsRestart = internalName == gPref.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN);
|
|
return needsRestart;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Perform initial configuration on an item that has just or will be
|
|
* installed. This inserts the item into the appropriate container in the
|
|
* datasource, so that the application UI shows the item even if it will
|
|
* not actually be installed until the next restart.
|
|
* @param installManifest
|
|
* The Install Manifest datasource that describes this item.
|
|
* @param id
|
|
* The GUID of this item.
|
|
* @param installLocation
|
|
* The Install Location where this item is installed.
|
|
* @param type
|
|
* The nsIUpdateItem type of this item.
|
|
*/
|
|
_configureForthcomingItem: function(installManifest, id, installLocation, type) {
|
|
var ds = this.datasource;
|
|
ds.updateVisibleList(id, installLocation.name, false);
|
|
var props = { name : EM_L(getManifestProperty(installManifest, "name")),
|
|
version : EM_L(getManifestProperty(installManifest, "version")),
|
|
installLocation : EM_L(installLocation.name),
|
|
type : EM_I(type),
|
|
availableUpdateURL : null,
|
|
availableUpdateHash : null,
|
|
availableUpdateVersion: null };
|
|
for (var p in props)
|
|
ds.setItemProperty(id, EM_R(p), props[p]);
|
|
ds.updateProperty(id, "displayDescription");
|
|
ds.updateProperty(id, "availableUpdateURL");
|
|
|
|
this._setOp(id, OP_NEEDS_INSTALL);
|
|
|
|
// Insert it into the child list NOW rather than later because:
|
|
// - extensions installed using the command line need to be a member
|
|
// of a container during the install phase for the code to be able
|
|
// to identify profile vs. global
|
|
// - extensions installed through the UI should show some kind of
|
|
// feedback to indicate their presence is forthcoming (i.e. they
|
|
// will be available after a restart).
|
|
ds.insertItemIntoContainer(id);
|
|
|
|
this._notifyAction(id, EM_ITEM_INSTALLED);
|
|
},
|
|
|
|
/**
|
|
* Perform configuration on an item that has just or will be upgraded.
|
|
* @param installManifest
|
|
* The Install Manifest datasource that describes this item.
|
|
* @param itemID
|
|
* The GUID of this item.
|
|
* @param installLocation
|
|
* The Install Location where this item is installed.
|
|
* @param type
|
|
* The nsIUpdateItem type of this item.
|
|
*/
|
|
_upgradeItem: function (installManifest, id, installLocation, type) {
|
|
// Don't change any props that would need to be reset if the install fails.
|
|
// They will be reset as appropriate by the upgrade/install process.
|
|
var ds = this.datasource;
|
|
ds.updateVisibleList(id, installLocation.name, false);
|
|
var props = { installLocation : EM_L(installLocation.name),
|
|
type : EM_I(type),
|
|
availableUpdateURL : null,
|
|
availableUpdateHash : null,
|
|
availableUpdateVersion : null };
|
|
for (var p in props)
|
|
ds.setItemProperty(id, EM_R(p), props[p]);
|
|
ds.updateProperty(id, "displayDescription");
|
|
ds.updateProperty(id, "availableUpdateURL");
|
|
|
|
this._setOp(id, OP_NEEDS_UPGRADE);
|
|
this._notifyAction(id, EM_ITEM_UPGRADED);
|
|
},
|
|
|
|
/**
|
|
* Completes an Extension's installation.
|
|
* @param id
|
|
* The GUID of the Extension to install.
|
|
* @param file
|
|
* The XPI/JAR file to install from. If this is null, we try to
|
|
* determine the stage file location from the ID.
|
|
*/
|
|
_finalizeInstall: function(id, file) {
|
|
var ds = this.datasource;
|
|
var type = ds.getItemProperty(id, "type");
|
|
if (id == 0 || id == -1) {
|
|
ds.removeCorruptItem(id, type);
|
|
return;
|
|
}
|
|
var installLocation = this.getInstallLocation(id);
|
|
if (!installLocation) {
|
|
// If the install location is null, that means we've reached the finalize
|
|
// state without the item ever having metadata added for it, which implies
|
|
// bogus data in the Startup Cache. Clear the entries and don't do anything
|
|
// else.
|
|
var entries = StartupCache.findEntries(id);
|
|
for (var i = 0; i < entries.length; ++i) {
|
|
var location = InstallLocations.get(entries[i].location);
|
|
StartupCache.clearEntry(location, id);
|
|
PendingOperations.clearItem(OP_NEEDS_INSTALL, id);
|
|
}
|
|
return;
|
|
}
|
|
var itemLocation = installLocation.getItemLocation(id);
|
|
|
|
if (!file && "stageFile" in installLocation)
|
|
file = installLocation.getStageFile(id);
|
|
|
|
// If |file| is null or does not exist, the installer assumes the item is
|
|
// a dropped-in directory.
|
|
var installer = new Installer(this.datasource, id, installLocation, type);
|
|
installer.installFromFile(file);
|
|
|
|
// If the file was staged, we must clean it up ourselves, otherwise the
|
|
// EM caller is responsible for doing so (e.g. XPInstall)
|
|
if (file)
|
|
installLocation.removeFile(file);
|
|
|
|
// Clear the op flag from the Startup Cache and Pending Operations sets
|
|
StartupCache.put(installLocation, id, OP_NONE, true);
|
|
PendingOperations.clearItem(OP_NEEDS_INSTALL, id);
|
|
},
|
|
|
|
/**
|
|
* Removes an item's metadata in preparation for an upgrade-install.
|
|
* @param id
|
|
* The GUID of the item to uninstall.
|
|
*/
|
|
_finalizeUpgrade: function(id) {
|
|
// Retrieve the item properties *BEFORE* we clean the resource!
|
|
var ds = this.datasource;
|
|
var installLocation = this.getInstallLocation(id);
|
|
|
|
var stagedFile = null;
|
|
if ("getStageFile" in installLocation)
|
|
stagedFile = installLocation.getStageFile(id);
|
|
|
|
if (stagedFile)
|
|
var installRDF = extractRDFFileToTempDir(stagedFile, FILE_INSTALL_MANIFEST, true);
|
|
else
|
|
installRDF = installLocation.getItemFile(id, FILE_INSTALL_MANIFEST);
|
|
if (installRDF.exists()) {
|
|
var installManifest = getInstallManifest(installRDF);
|
|
if (installManifest) {
|
|
var type = getAddonTypeFromInstallManifest(installManifest);
|
|
var userDisabled = ds.getItemProperty(id, "userDisabled") == "true";
|
|
|
|
// Clean the item resource
|
|
ds.removeItemMetadata(id);
|
|
// Now set up the properties on the item to mimic an item in its
|
|
// "initial state" for installation.
|
|
this._configureForthcomingItem(installManifest, id, installLocation,
|
|
type);
|
|
if (userDisabled)
|
|
ds.setItemProperty(id, EM_R("userDisabled"), EM_L("true"));
|
|
}
|
|
if (stagedFile)
|
|
installRDF.remove(false);
|
|
}
|
|
// Clear the op flag from the Pending Operations set. Do NOT clear op flag in
|
|
// the startup cache since this may have been reset to OP_NEEDS_INSTALL by
|
|
// |_configureForthcomingItem|.
|
|
PendingOperations.clearItem(OP_NEEDS_UPGRADE, id);
|
|
},
|
|
|
|
/**
|
|
* Completes an item's uninstallation.
|
|
* @param id
|
|
* The GUID of the item to uninstall.
|
|
*/
|
|
_finalizeUninstall: function(id) {
|
|
var ds = this.datasource;
|
|
|
|
var installLocation = this.getInstallLocation(id);
|
|
if (!installLocation.itemIsManagedIndependently(id)) {
|
|
try {
|
|
// Having a callback that does nothing just causes the directory to be
|
|
// removed.
|
|
safeInstallOperation(id, installLocation,
|
|
{ data: null, callback: function() { } });
|
|
}
|
|
catch (e) {
|
|
LOG("_finalizeUninstall: failed to remove directory for item: " + id +
|
|
" at Install Location: " + installLocation.name + ", rolling back uninstall");
|
|
// Removal of the files failed, reset the uninstalled flag and rewrite
|
|
// the install manifests so this item's components are registered.
|
|
// Clear the op flag from the Startup Cache
|
|
StartupCache.put(installLocation, id, OP_NONE, true);
|
|
var restartRequired = this.installRequiresRestart(id, ds.getItemProperty(id, "type"))
|
|
this._updateManifests(restartRequired);
|
|
return;
|
|
}
|
|
}
|
|
else if (installLocation.name == KEY_APP_PROFILE ||
|
|
installLocation.name == KEY_APP_GLOBAL) {
|
|
// Check for a pointer file and remove it if it exists
|
|
var pointerFile = installLocation.location.clone();
|
|
pointerFile.append(id);
|
|
if (pointerFile.exists() && !pointerFile.isDirectory())
|
|
pointerFile.remove(false);
|
|
}
|
|
|
|
// Clean the item resource
|
|
ds.removeItemMetadata(id);
|
|
|
|
// Do this LAST since inferences are made about an item based on
|
|
// what container it's in.
|
|
ds.removeItemFromContainer(id);
|
|
|
|
// Clear the op flag from the Startup Cache and the Pending Operations set.
|
|
StartupCache.clearEntry(installLocation, id);
|
|
PendingOperations.clearItem(OP_NEEDS_UNINSTALL, id);
|
|
},
|
|
|
|
/**
|
|
* Uninstalls an item. If the uninstallation cannot be performed immediately
|
|
* it is scheduled for the next restart.
|
|
* @param id
|
|
* The GUID of the item to uninstall.
|
|
*/
|
|
uninstallItem: function(id) {
|
|
var em = this;
|
|
/**
|
|
* Cleans up any staged xpis for this item when we uninstall. This fixes
|
|
* this scenario:
|
|
* 1 install Foo 1.0, and restart
|
|
* 2 install Foo 1.1,
|
|
* 3 without restarting, uninstall Foo 1.1
|
|
* 4 Foo is uninstalled but the staged XPI from the install operation in
|
|
* step 2 lingers, and is offered to the user on the next startup to
|
|
* be installed.
|
|
* @param id
|
|
* The GUID of the item to look for staged files for.
|
|
*/
|
|
function cleanUpPendingStageFile(id) {
|
|
var foundStageOp = false;
|
|
var stageOps = PendingOperations.getOperations(OP_NEEDS_UPGRADE);
|
|
stageOps = stageOps.concat(PendingOperations.getOperations(OP_NEEDS_INSTALL));
|
|
for (var i = 0; i < stageOps.length; ++i) {
|
|
if (stageOps[i].id == id) {
|
|
foundStageOp = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundStageOp) {
|
|
var location = em.getInstallLocation(id);
|
|
var stageFile = location.getStageFile(id);
|
|
if (stageFile)
|
|
location.removeFile(stageFile);
|
|
}
|
|
}
|
|
|
|
var ds = this.datasource;
|
|
var type = ds.getItemProperty(id, "type");
|
|
if (!ds.isDownloadItem(id)) {
|
|
cleanUpPendingStageFile(id);
|
|
this._setOp(id, OP_NEEDS_UNINSTALL);
|
|
var restartRequired = this.installRequiresRestart(id, type);
|
|
if (!restartRequired) {
|
|
this._finalizeUninstall(id);
|
|
this._updateManifests(restartRequired);
|
|
}
|
|
}
|
|
else {
|
|
// Bad download entry - uri is url, e.g. "http://www.foo.com/test.xpi"
|
|
// ... just remove it from the list.
|
|
ds.removeCorruptDLItem(id);
|
|
}
|
|
|
|
this._notifyAction(id, EM_ITEM_UNINSTALLED);
|
|
},
|
|
|
|
/**
|
|
* Sets the pending operation for a visible item.
|
|
* @param id
|
|
* The GUID of the item
|
|
* @param op
|
|
* The name of the operation to be performed
|
|
*/
|
|
_setOp: function(id, op) {
|
|
var location = this.getInstallLocation(id);
|
|
StartupCache.put(location, id, op, true);
|
|
PendingOperations.addItem(op, { locationKey: location.name, id: id });
|
|
var ds = this.datasource;
|
|
ds.updateProperty(id, "opType");
|
|
ds.updateProperty(id, "updateable");
|
|
ds.updateProperty(id, "displayDescription");
|
|
var restartRequired = this.installRequiresRestart(id, ds.getItemProperty(id, "type"))
|
|
this._updateManifests(restartRequired);
|
|
},
|
|
|
|
/**
|
|
* Enables an item for the application (e.g. the item meets all requirements
|
|
* for it to be enabled). If the item is not disabled by the user this will
|
|
* also set the needs-enable operation for the next restart.
|
|
* @param id
|
|
* The ID of the item to be enabled by the application.
|
|
*/
|
|
_appEnableItem: function(id) {
|
|
var ds = this.datasource;
|
|
if (ds.getItemProperty(id, "userDisabled") != "true") {
|
|
this._setOp(id, OP_NEEDS_ENABLE);
|
|
this._notifyAction(id, EM_ITEM_ENABLED);
|
|
}
|
|
else {
|
|
ds.setItemProperty(id, EM_R("appDisabled"), null);
|
|
ds.updateProperty(id, "compatible");
|
|
ds.updateProperty(id, "displayDescription");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Disables an item for the application (e.g. the item does not meets all
|
|
* requirements like app compatibility for it to be enabled). If the item is
|
|
* not disabled by the user this will also set the needs-disable operation
|
|
* for the next restart.
|
|
* @param id
|
|
* The ID of the item to be disabled by the application.
|
|
*/
|
|
_appDisableItem: function(id) {
|
|
var ds = this.datasource;
|
|
ds.setItemProperty(id, EM_R("appDisabled"), EM_L("true"));
|
|
if (ds.getItemProperty(id, "userDisabled") != "true") {
|
|
this._setOp(id, OP_NEEDS_DISABLE);
|
|
this._notifyAction(id, EM_ITEM_DISABLED);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets an item to be enabled by the user. If the item is already enabled this
|
|
* clears the needs-enable operation for the next restart. If the item's
|
|
* operation is set to needs-uninstall this will cancel the uninstall and set
|
|
* the needs-enable operation for the next restart if the item is disabled.
|
|
*
|
|
* @param id
|
|
* The ID of the item to be enabled by the user.
|
|
*/
|
|
enableItem: function(id) {
|
|
var ds = this.datasource;
|
|
if (ds.getItemProperty(id, "userDisabled") == "true" ||
|
|
ds.getItemProperty(id, "appDisabled") == "true" &&
|
|
ds.getItemProperty(id, "compatible") == "true") {
|
|
this._setOp(id, OP_NEEDS_ENABLE);
|
|
this._notifyAction(id, EM_ITEM_ENABLED);
|
|
}
|
|
else {
|
|
this._setOp(id, OP_NONE);
|
|
this._notifyAction(id, EM_ITEM_CANCEL);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets an item to be disabled by the user. If the item is already disabled
|
|
* this clears the needs-disable operation for the next restart.
|
|
* @param id
|
|
* The ID of the item to be disabled by the user.
|
|
*/
|
|
disableItem: function(id) {
|
|
var ds = this.datasource;
|
|
if (ds.getItemProperty(id, "userDisabled") == "true") {
|
|
this._setOp(id, OP_NONE);
|
|
this._notifyAction(id, EM_ITEM_CANCEL);
|
|
}
|
|
else {
|
|
this._setOp(id, OP_NEEDS_DISABLE);
|
|
this._notifyAction(id, EM_ITEM_DISABLED);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Notify observers of a change to an item that has been requested by the
|
|
* user.
|
|
*/
|
|
_notifyAction: function(id, reason) {
|
|
gOS.notifyObservers(this.datasource.getItemForID(id),
|
|
EM_ACTION_REQUESTED_TOPIC, reason);
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
update: function(items, itemCount, versionUpdateOnly, listener) {
|
|
var appID = gApp.ID;
|
|
var appVersion = getPref("getCharPref", PREF_EM_APP_EXTENSIONS_VERSION,
|
|
gApp.version);
|
|
|
|
if (items.length == 0)
|
|
items = this.getItemList(nsIUpdateItem.TYPE_ADDON, { });
|
|
|
|
var updater = new ExtensionItemUpdater(appID, appVersion, this);
|
|
updater.checkForUpdates(items, items.length, versionUpdateOnly, listener);
|
|
},
|
|
|
|
/**
|
|
* @returns An enumeration of all registered Install Locations.
|
|
*/
|
|
get installLocations () {
|
|
return InstallLocations.enumeration;
|
|
},
|
|
|
|
/**
|
|
* Gets the Install Location where a visible Item is stored.
|
|
* @param id
|
|
* The GUID of the item to locate an Install Location for.
|
|
* @returns The Install Location object where the item is stored.
|
|
*/
|
|
getInstallLocation: function(id) {
|
|
var key = this.datasource.visibleItems[id];
|
|
return key ? InstallLocations.get(this.datasource.visibleItems[id]) : null;
|
|
},
|
|
|
|
/**
|
|
* Gets a nsIUpdateItem for the item with the specified id.
|
|
* @param id
|
|
* The GUID of the item to construct a nsIUpdateItem for.
|
|
* @returns The nsIUpdateItem representing the item.
|
|
*/
|
|
getItemForID: function(id) {
|
|
return this.datasource.getItemForID(id);
|
|
},
|
|
|
|
/**
|
|
* Retrieves a list of nsIUpdateItems of items matching the specified type.
|
|
* @param type
|
|
* The type of item to return.
|
|
* @param countRef
|
|
* The XPCJS reference to the number of items returned.
|
|
* @returns An array of nsIUpdateItems matching the id/type filter.
|
|
*/
|
|
getItemList: function(type, countRef) {
|
|
return this.datasource.getItemList(type, countRef);
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
getIncompatibleItemList: function(id, version, type, includeDisabled,
|
|
countRef) {
|
|
var items = this.datasource.getIncompatibleItemList(id, version ? version : undefined,
|
|
type, includeDisabled);
|
|
countRef.value = items.length;
|
|
return items;
|
|
},
|
|
|
|
/**
|
|
* Move an Item to the index of another item in its container.
|
|
* @param movingID
|
|
* The ID of the item to be moved.
|
|
* @param destinationID
|
|
* The ID of an item to move another item to.
|
|
*/
|
|
moveToIndexOf: function(movingID, destinationID) {
|
|
this.datasource.moveToIndexOf(movingID, destinationID);
|
|
},
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// Downloads
|
|
_transactions: [],
|
|
_downloadCount: 0,
|
|
|
|
/**
|
|
* Ask the user if they really want to quit the application, since this will
|
|
* cancel one or more Extension/Theme downloads.
|
|
* @param subject
|
|
* A nsISupportsPRBool which this function sets to false if the user
|
|
* wishes to cancel all active downloads and quit the application,
|
|
* false otherwise.
|
|
*/
|
|
_confirmCancelDownloadsOnQuit: function(subject) {
|
|
if (this._downloadCount > 0) {
|
|
var result;
|
|
//@line 4663 "/cygdrive/c/mozilla/toolkit/mozapps/extensions/src/nsExtensionManager.js.in"
|
|
result = this._confirmCancelDownloads(this._downloadCount,
|
|
"quitCancelDownloadsAlertTitle",
|
|
"quitCancelDownloadsAlertMsgMultiple",
|
|
"quitCancelDownloadsAlertMsg",
|
|
"dontQuitButtonWin");
|
|
//@line 4675 "/cygdrive/c/mozilla/toolkit/mozapps/extensions/src/nsExtensionManager.js.in"
|
|
if (!result)
|
|
this._cancelDownloads();
|
|
if (subject instanceof Components.interfaces.nsISupportsPRBool)
|
|
subject.data = result;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Ask the user if they really want to go offline, since this will cancel
|
|
* one or more Extension/Theme downloads.
|
|
* @param subject
|
|
* A nsISupportsPRBool which this function sets to false if the user
|
|
* wishes to cancel all active downloads and go offline, false
|
|
* otherwise.
|
|
*/
|
|
_confirmCancelDownloadsOnOffline: function(subject) {
|
|
if (this._downloadCount > 0) {
|
|
result = this._confirmCancelDownloads(this._downloadCount,
|
|
"offlineCancelDownloadsAlertTitle",
|
|
"offlineCancelDownloadsAlertMsgMultiple",
|
|
"offlineCancelDownloadsAlertMsg",
|
|
"dontGoOfflineButton");
|
|
if (!result)
|
|
this._cancelDownloads();
|
|
var PRBool = subject.QueryInterface(Components.interfaces.nsISupportsPRBool);
|
|
PRBool.data = result;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Cancels all active downloads and removes them from the applicable UI.
|
|
*/
|
|
_cancelDownloads: function() {
|
|
for (var i = 0; i < this._transactions.length; ++i)
|
|
gOS.notifyObservers(this._transactions[i], "xpinstall-progress", "cancel");
|
|
gOS.removeObserver(this, "offline-requested");
|
|
gOS.removeObserver(this, "quit-application-requested");
|
|
|
|
this._removeAllDownloads();
|
|
},
|
|
|
|
/**
|
|
* Ask the user whether or not they wish to cancel the Extension/Theme
|
|
* downloads which are currently under way.
|
|
* @param count
|
|
* The number of active downloads.
|
|
* @param title
|
|
* The key of the title for the message box to be displayed
|
|
* @param cancelMessageMultiple
|
|
* The key of the message to be displayed in the message box
|
|
* when there are > 1 active downloads.
|
|
* @param cancelMessageSingle
|
|
* The key of the message to be displayed in the message box
|
|
* when there is just one active download.
|
|
* @param dontCancelButton
|
|
* The key of the label to be displayed on the "Don't Cancel
|
|
* Downloads" button.
|
|
*/
|
|
_confirmCancelDownloads: function(count, title, cancelMessageMultiple,
|
|
cancelMessageSingle, dontCancelButton) {
|
|
var bundle = BundleManager.getBundle(URI_DOWNLOADS_PROPERTIES);
|
|
var title = bundle.GetStringFromName(title);
|
|
var message, quitButton;
|
|
if (count > 1) {
|
|
message = bundle.formatStringFromName(cancelMessageMultiple, [count], 1);
|
|
quitButton = bundle.formatStringFromName("cancelDownloadsOKTextMultiple", [count], 1);
|
|
}
|
|
else {
|
|
message = bundle.GetStringFromName(cancelMessageSingle);
|
|
quitButton = bundle.GetStringFromName("cancelDownloadsOKText");
|
|
}
|
|
var dontQuitButton = bundle.GetStringFromName(dontCancelButton);
|
|
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
var win = wm.getMostRecentWindow("Extension:Manager");
|
|
const nsIPromptService = Components.interfaces.nsIPromptService;
|
|
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
|
.getService(nsIPromptService);
|
|
var flags = (nsIPromptService.BUTTON_TITLE_IS_STRING * nsIPromptService.BUTTON_POS_0) +
|
|
(nsIPromptService.BUTTON_TITLE_IS_STRING * nsIPromptService.BUTTON_POS_1);
|
|
var rv = ps.confirmEx(win, title, message, flags, quitButton, dontQuitButton, null, null, { });
|
|
return rv == 1;
|
|
},
|
|
|
|
/**
|
|
* Adds a set of Item Downloads to the Manager and starts the download
|
|
* operation.
|
|
* @param items
|
|
* An array of nsIUpdateItems to begin downlading.
|
|
* @param itemCount
|
|
* The length of |items|
|
|
* @param fromChrome
|
|
* true when called from chrome
|
|
* false when not called from chrome (e.g. web page)
|
|
*/
|
|
addDownloads: function(items, itemCount, fromChrome) {
|
|
this._downloadCount += itemCount;
|
|
|
|
var urls = [];
|
|
var hashes = [];
|
|
var txn = new ItemDownloadTransaction(this);
|
|
for (var i = 0; i < itemCount; ++i) {
|
|
var currItem = items[i];
|
|
var txnID = Math.round(Math.random() * 100);
|
|
txn.addDownload(currItem, txnID);
|
|
this._transactions.push(txn);
|
|
urls.push(currItem.xpiURL);
|
|
hashes.push(currItem.xpiHash ? currItem.xpiHash : null);
|
|
}
|
|
|
|
// Kick off the download process for this transaction
|
|
gOS.addObserver(this, "offline-requested", false);
|
|
gOS.addObserver(this, "quit-application-requested", false);
|
|
|
|
if (fromChrome) {
|
|
// Initiate an install from chrome
|
|
var xpimgr =
|
|
Components.classes["@mozilla.org/xpinstall/install-manager;1"].
|
|
createInstance(Components.interfaces.nsIXPInstallManager);
|
|
xpimgr.initManagerWithHashes(urls, hashes, urls.length, txn);
|
|
}
|
|
else
|
|
gOS.notifyObservers(txn, "xpinstall-progress", "open");
|
|
},
|
|
|
|
/**
|
|
* Removes a download of a URL.
|
|
* @param url
|
|
* The URL of the item being downloaded to remove.
|
|
*/
|
|
removeDownload: function(url) {
|
|
for (var i = 0; i < this._transactions.length; ++i) {
|
|
if (this._transactions[i].containsURL(url)) {
|
|
this._transactions[i].removeDownload(url);
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove all downloads from all transactions.
|
|
*/
|
|
_removeAllDownloads: function() {
|
|
for (var i = 0; i < this._transactions.length; ++i)
|
|
this._transactions[i].removeAllDownloads();
|
|
},
|
|
|
|
/**
|
|
* Download Operation State has changed from one to another.
|
|
*
|
|
* The nsIXPIProgressDialog implementation in the download transaction object
|
|
* forwards notifications through these methods which we then pass on to any
|
|
* front end objects implementing nsIExtensionDownloadListener that
|
|
* are listening. We maintain the master state of download operations HERE,
|
|
* not in the front end, because if the user closes the extension or theme
|
|
* managers during the downloads we need to maintain state and not terminate
|
|
* the download/install process.
|
|
*
|
|
* @param transaction
|
|
* The ItemDownloadTransaction object receiving the download
|
|
* notifications from XPInstall.
|
|
* @param addon
|
|
* An object representing nsIUpdateItem for the addon being updated
|
|
* @param state
|
|
* The state we are entering
|
|
* @param value
|
|
* ???
|
|
*/
|
|
onStateChange: function(transaction, addon, state, value) {
|
|
var url = addon.xpiURL;
|
|
if (!(url in this._progressData))
|
|
this._progressData[url] = { };
|
|
this._progressData[url].state = state;
|
|
|
|
for (var i = 0; i < this._updateListeners.length; ++i)
|
|
this._updateListeners[i].onStateChange(addon, state, value);
|
|
|
|
const nsIXPIProgressDialog = Components.interfaces.nsIXPIProgressDialog;
|
|
switch (state) {
|
|
case nsIXPIProgressDialog.INSTALL_DONE:
|
|
--this._downloadCount;
|
|
break;
|
|
case nsIXPIProgressDialog.DIALOG_CLOSE:
|
|
for (var i = 0; i < this._transactions.length; ++i) {
|
|
if (this._transactions[i].id == transaction.id) {
|
|
this._transactions.splice(i, 1);
|
|
delete transaction;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
// If we're updating an installed item for which content is already built,
|
|
// update the "displayDescription" property so the restart now message is
|
|
// shown.
|
|
if (addon.id != addon.xpiURL) {
|
|
var ds = this.datasource;
|
|
ds.updateProperty(addon.id, "displayDescription");
|
|
ds.updateProperty(addon.id, "availableUpdateURL");
|
|
ds.updateProperty(addon.id, "updateable");
|
|
}
|
|
},
|
|
|
|
_progressData: { },
|
|
onProgress: function(addon, value, maxValue) {
|
|
for (var i = 0; i < this._updateListeners.length; ++i)
|
|
this._updateListeners[i].onProgress(addon, value, maxValue);
|
|
|
|
var url = addon.xpiURL;
|
|
if (!(url in this._progressData))
|
|
this._progressData[url] = { };
|
|
this._progressData[url].progress = Math.round((value / maxValue) * 100);
|
|
},
|
|
|
|
_updateListeners: [],
|
|
addUpdateListener: function(listener) {
|
|
for (var i = 0; i < this._updateListeners.length; ++i) {
|
|
if (this._updateListeners[i] == listener)
|
|
return i;
|
|
}
|
|
this._updateListeners.push(listener);
|
|
return this._updateListeners.length - 1;
|
|
},
|
|
|
|
removeUpdateListenerAt: function(index) {
|
|
this._updateListeners.splice(index, 1);
|
|
if (this._downloadCount != 0)
|
|
this.datasource.flushProgressInfo(this._progressData);
|
|
},
|
|
|
|
/**
|
|
* The Extensions RDF Datasource
|
|
*/
|
|
_ds: null,
|
|
|
|
/**
|
|
* Loads the Extensions Datasource. This should not be called unless:
|
|
* - a piece of Extensions UI is being shown, or
|
|
* - on startup and there has been a change to an Install Location
|
|
* ... it should NOT be called on every startup!
|
|
*/
|
|
_ensureDS: function() {
|
|
if (!this._ds) {
|
|
this._ds = new ExtensionsDataSource(this);
|
|
if (this._ds)
|
|
this._ds.loadExtensions();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
get datasource() {
|
|
this._ensureDS();
|
|
return this._ds.QueryInterface(Components.interfaces.nsIRDFDataSource);
|
|
},
|
|
|
|
/**
|
|
* See nsIClassInfo.idl
|
|
*/
|
|
getInterfaces: function(count) {
|
|
var interfaces = [Components.interfaces.nsIExtensionManager,
|
|
Components.interfaces.nsIXPIProgressDialog,
|
|
Components.interfaces.nsIObserver];
|
|
count.value = interfaces.length;
|
|
return interfaces;
|
|
},
|
|
getHelperForLanguage: function(language) {
|
|
return null;
|
|
},
|
|
get contractID() {
|
|
return "@mozilla.org/extensions/manager;1";
|
|
},
|
|
get classDescription() {
|
|
return "Extension Manager";
|
|
},
|
|
get classID() {
|
|
return Components.ID("{8A115FAA-7DCB-4e8f-979B-5F53472F51CF}");
|
|
},
|
|
get implementationLanguage() {
|
|
return Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT;
|
|
},
|
|
get flags() {
|
|
return Components.interfaces.nsIClassInfo.SINGLETON;
|
|
},
|
|
|
|
/**
|
|
* See nsISupports.idl
|
|
*/
|
|
QueryInterface: function(iid) {
|
|
if (!iid.equals(Components.interfaces.nsIExtensionManager) &&
|
|
!iid.equals(Components.interfaces.nsITimerCallback) &&
|
|
!iid.equals(Components.interfaces.nsIObserver) &&
|
|
!iid.equals(Components.interfaces.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This object implements nsIXPIProgressDialog and represents a collection of
|
|
* XPI/JAR download and install operations. There is one
|
|
* ItemDownloadTransaction per back-end XPInstallManager object. We maintain
|
|
* a collection of separate transaction objects because it's possible to have
|
|
* multiple separate XPInstall download/install operations going on
|
|
* simultaneously, each with its own XPInstallManager instance. For instance
|
|
* you could start downloading two extensions and then download a theme. Each
|
|
* of these operations would open the appropriate FE and have to be able to
|
|
* track each operation independently.
|
|
*
|
|
* @constructor
|
|
*/
|
|
function ItemDownloadTransaction(manager) {
|
|
this._manager = manager;
|
|
this._downloads = [];
|
|
}
|
|
ItemDownloadTransaction.prototype = {
|
|
_manager : null,
|
|
_downloads : [],
|
|
id : -1,
|
|
|
|
/**
|
|
* Add a download to this transaction
|
|
* @param addon
|
|
* An object implementing nsIUpdateItem for the item to be downloaded
|
|
* @param id
|
|
* The integer identifier of this transaction
|
|
*/
|
|
addDownload: function(addon, id) {
|
|
this._downloads.push({ addon: addon, waiting: true });
|
|
this._manager.datasource.addDownload(addon);
|
|
this.id = id;
|
|
},
|
|
|
|
/**
|
|
* Removes a download from this transaction
|
|
* @param url
|
|
* The URL to remove
|
|
*/
|
|
removeDownload: function(url) {
|
|
this._manager.datasource.removeDownload(url);
|
|
},
|
|
|
|
/**
|
|
* Remove all downloads from this transaction
|
|
*/
|
|
removeAllDownloads: function() {
|
|
for (var i = 0; i < this._downloads.length; ++i) {
|
|
var addon = this._downloads[i].addon;
|
|
this.removeDownload(addon.xpiURL, addon.type);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Determine if this transaction is handling the download of a url.
|
|
* @param url
|
|
* The URL to look for
|
|
* @returns true if this transaction is downloading the supplied url.
|
|
*/
|
|
containsURL: function(url) {
|
|
for (var i = 0; i < this._downloads.length; ++i) {
|
|
if (this._downloads[i].addon.xpiURL == url)
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* See nsIXPIProgressDialog.idl
|
|
*/
|
|
onStateChange: function(index, state, value) {
|
|
this._manager.onStateChange(this, this._downloads[index].addon,
|
|
state, value);
|
|
},
|
|
|
|
/**
|
|
* See nsIXPIProgressDialog.idl
|
|
*/
|
|
onProgress: function(index, value, maxValue) {
|
|
this._manager.onProgress(this._downloads[index].addon, value, maxValue);
|
|
},
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// nsISupports
|
|
QueryInterface: function(iid) {
|
|
if (!iid.equals(Components.interfaces.nsIXPIProgressDialog) &&
|
|
!iid.equals(Components.interfaces.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A listener object to the update check process that routes notifications to
|
|
* the right places and keeps the datasource up to date.
|
|
*/
|
|
function AddonUpdateCheckListener(listener, datasource) {
|
|
this._listener = listener;
|
|
this._ds = datasource;
|
|
}
|
|
AddonUpdateCheckListener.prototype = {
|
|
_listener: null,
|
|
_ds: null,
|
|
|
|
onUpdateStarted: function() {
|
|
if (this._listener)
|
|
this._listener.onUpdateStarted();
|
|
this._ds.onUpdateStarted();
|
|
},
|
|
|
|
onUpdateEnded: function() {
|
|
if (this._listener)
|
|
this._listener.onUpdateEnded();
|
|
this._ds.onUpdateEnded();
|
|
},
|
|
|
|
onAddonUpdateStarted: function(addon) {
|
|
if (this._listener)
|
|
this._listener.onAddonUpdateStarted(addon);
|
|
this._ds.onAddonUpdateStarted(addon);
|
|
},
|
|
|
|
onAddonUpdateEnded: function(addon, status) {
|
|
if (this._listener)
|
|
this._listener.onAddonUpdateEnded(addon, status);
|
|
this._ds.onAddonUpdateEnded(addon, status);
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// ExtensionItemUpdater
|
|
//
|
|
function ExtensionItemUpdater(aTargetAppID, aTargetAppVersion, aEM)
|
|
{
|
|
this._appID = aTargetAppID;
|
|
this._appVersion = aTargetAppVersion;
|
|
this._emDS = aEM._ds;
|
|
this._em = aEM;
|
|
|
|
getVersionChecker();
|
|
}
|
|
|
|
ExtensionItemUpdater.prototype = {
|
|
_appID : "",
|
|
_appVersion : "",
|
|
_emDS : null,
|
|
_em : null,
|
|
_versionUpdateOnly : 0,
|
|
_items : [],
|
|
_listener : null,
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// ExtensionItemUpdater
|
|
//
|
|
// When we check for updates to an item, there are two pieces of information
|
|
// that are returned - 1) info about the newest available version, if any,
|
|
// and 2) info about the currently installed version. The latter is provided
|
|
// primarily to inform the client of changes to the application compatibility
|
|
// metadata for the current item. Depending on the situation, either 2 or
|
|
// 1&2 may be what is required.
|
|
//
|
|
// Callers:
|
|
// 1 - nsUpdateService.js, user event
|
|
// User clicked on the update icon to invoke an update check,
|
|
// user clicked on an Extension/Theme and clicked "Update". In this
|
|
// case we want to update compatibility metadata about the installed
|
|
// version, and look for newer versions to offer.
|
|
// 2 - nsUpdateService.js, background event
|
|
// Timer fired, background update is being performed. In this case
|
|
// we also want to update compatibility metadata and look for newer
|
|
// versions.
|
|
// 3 - Mismatch
|
|
// User upgraded to a newer version of the app, update compatibility
|
|
// metadata and look for newer versions.
|
|
// 4 - Install Phone Home
|
|
// User installed an item that was deemed incompatible based only
|
|
// on the information provided in the item's install.rdf manifest,
|
|
// we look ONLY for compatibility updates in this case to determine
|
|
// whether or not the item can be installed.
|
|
//
|
|
checkForUpdates: function(aItems, aItemCount, aVersionUpdateOnly,
|
|
aListener) {
|
|
this._listener = new AddonUpdateCheckListener(aListener, this._emDS);
|
|
if (this._listener)
|
|
this._listener.onUpdateStarted();
|
|
this._versionUpdateOnly = aVersionUpdateOnly;
|
|
this._items = aItems;
|
|
this._responseCount = aItemCount;
|
|
|
|
// This is the number of extensions/themes/etc that we found updates for.
|
|
this._updateCount = 0;
|
|
|
|
for (var i = 0; i < aItemCount; ++i) {
|
|
var e = this._items[i];
|
|
if (this._listener)
|
|
this._listener.onAddonUpdateStarted(e);
|
|
(new RDFItemUpdater(this)).checkForUpdates(e, aVersionUpdateOnly);
|
|
}
|
|
},
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// ExtensionItemUpdater
|
|
_applyVersionUpdates: function(aLocalItem, aRemoteItem) {
|
|
var targetAppInfo = this._emDS.getTargetApplicationInfo(aLocalItem.id, this._emDS);
|
|
// If targetAppInfo is null this is for a new install. If the local item's
|
|
// maxVersion does not equal the targetAppInfo maxVersion then this is for
|
|
// an upgrade. In both of these cases return true if the remotely specified
|
|
// maxVersion is greater than the local item's maxVersion.
|
|
if (!targetAppInfo ||
|
|
gVersionChecker.compare(aLocalItem.maxAppVersion, targetAppInfo.maxVersion) != 0) {
|
|
if (gVersionChecker.compare(aLocalItem.maxAppVersion, aRemoteItem.maxAppVersion) < 0)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
if (gVersionChecker.compare(targetAppInfo.maxVersion, aRemoteItem.maxAppVersion) < 0) {
|
|
// Remotely specified maxVersion is newer than the maxVersion
|
|
// for the installed Extension. Apply that change to the datasources.
|
|
this._emDS.updateTargetAppInfo(aLocalItem.id, aRemoteItem.minAppVersion,
|
|
aRemoteItem.maxAppVersion);
|
|
|
|
// If we got here through |checkForMismatches|, this extension has
|
|
// already been disabled, re-enable it.
|
|
var op = StartupCache.entries[aLocalItem.installLocationKey][aLocalItem.id].op;
|
|
if (op == OP_NEEDS_DISABLE ||
|
|
this._emDS.getItemProperty(aLocalItem.id, "appDisabled") == "true")
|
|
this._em._appEnableItem(aLocalItem.id);
|
|
return true;
|
|
}
|
|
else if (this._versionUpdateOnly == 2)
|
|
this._emDS.updateTargetAppInfo(aLocalItem.id, aRemoteItem.minAppVersion,
|
|
aRemoteItem.maxAppVersion);
|
|
return false;
|
|
},
|
|
|
|
_isValidUpdate: function(aLocalItem, aRemoteItem) {
|
|
var appExtensionsVersion =
|
|
getPref("getCharPref", PREF_EM_APP_EXTENSIONS_VERSION, gApp.version);
|
|
|
|
// Check if the update will only run on a newer version of Firefox.
|
|
if (aRemoteItem.minAppVersion &&
|
|
gVersionChecker.compare(appExtensionsVersion, aRemoteItem.minAppVersion) < 0)
|
|
return false;
|
|
|
|
// Check if the update will only run on an older version of Firefox.
|
|
if (aRemoteItem.maxAppVersion &&
|
|
gVersionChecker.compare(appExtensionsVersion, aRemoteItem.maxAppVersion) > 0)
|
|
return false;
|
|
|
|
return true;
|
|
},
|
|
|
|
checkForDone: function(item, status) {
|
|
if (this._listener) {
|
|
try {
|
|
this._listener.onAddonUpdateEnded(item, status);
|
|
}
|
|
catch (e) {
|
|
LOG("ExtensionItemUpdater:checkForDone: Failure in listener's onAddonUpdateEnded: " + e);
|
|
}
|
|
}
|
|
if (--this._responseCount == 0 && this._listener) {
|
|
try {
|
|
this._listener.onUpdateEnded();
|
|
}
|
|
catch (e) {
|
|
LOG("ExtensionItemUpdater:checkForDone: Failure in listener's onUpdateEnded: " + e);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
function RDFItemUpdater(aUpdater) {
|
|
this._updater = aUpdater;
|
|
}
|
|
|
|
RDFItemUpdater.prototype = {
|
|
_updater : null,
|
|
_versionUpdateOnly : 0,
|
|
_item : null,
|
|
|
|
checkForUpdates: function(aItem, aVersionUpdateOnly) {
|
|
// A preference setting can disable updating for this item
|
|
try {
|
|
if (!gPref.getBoolPref(PREF_EM_ITEM_UPDATE_ENABLED.replace(/%UUID%/, aItem.id))) {
|
|
var status = nsIAddonUpdateCheckListener.STATUS_DISABLED;
|
|
this._updater.checkForDone(aItem, status);
|
|
return;
|
|
}
|
|
}
|
|
catch (e) { }
|
|
|
|
// Items managed by the app are not checked for updates.
|
|
var emDS = this._updater._emDS;
|
|
if (emDS.getItemProperty(aItem.id, "appManaged") == "true") {
|
|
var status = nsIAddonUpdateCheckListener.STATUS_APP_MANAGED;
|
|
this._updater.checkForDone(aItem, status);
|
|
return;
|
|
}
|
|
|
|
// Items that have a pending install, uninstall, or upgrade are not checked
|
|
// for updates.
|
|
var opType = emDS.getItemProperty(aItem.id, "opType");
|
|
if (opType == OP_NEEDS_INSTALL || opType == OP_NEEDS_UNINSTALL ||
|
|
opType == OP_NEEDS_UPGRADE) {
|
|
var status = nsIAddonUpdateCheckListener.STATUS_PENDING_OP;
|
|
this._updater.checkForDone(aItem, status);
|
|
return;
|
|
}
|
|
|
|
var installLocation = InstallLocations.get(emDS.getInstallLocationKey(aItem.id));
|
|
// Don't check items for updates that are installed in a location that is
|
|
// not managed by the app.
|
|
if (installLocation && (installLocation.name == "winreg-app-global" ||
|
|
installLocation.name == "winreg-app-user")) {
|
|
var status = nsIAddonUpdateCheckListener.STATUS_NOT_MANAGED;
|
|
this._updater.checkForDone(aItem, status);
|
|
return;
|
|
}
|
|
|
|
// Don't check items for updates if the location can't be written to except
|
|
// when performing a version only update.
|
|
if (!aVersionUpdateOnly && (!installLocation || !installLocation.canAccess)) {
|
|
var status = nsIAddonUpdateCheckListener.STATUS_READ_ONLY;
|
|
this._updater.checkForDone(aItem, status);
|
|
return;
|
|
}
|
|
|
|
this._versionUpdateOnly = aVersionUpdateOnly;
|
|
this._item = aItem;
|
|
|
|
// Look for a custom update URI: 1) supplied by a pref, 2) supplied by the
|
|
// install manifest, 3) the default configuration
|
|
try {
|
|
var dsURI = gPref.getComplexValue(PREF_EM_ITEM_UPDATE_URL.replace(/%UUID%/, aItem.id),
|
|
Components.interfaces.nsIPrefLocalizedString).data;
|
|
}
|
|
catch (e) { }
|
|
if (!dsURI)
|
|
dsURI = aItem.updateRDF;
|
|
if (!dsURI) {
|
|
dsURI = gPref.getComplexValue(PREF_UPDATE_DEFAULT_URL,
|
|
Components.interfaces.nsIPrefLocalizedString).data;
|
|
}
|
|
dsURI = dsURI.replace(/%ITEM_ID%/g, aItem.id);
|
|
dsURI = dsURI.replace(/%ITEM_VERSION%/g, aItem.version);
|
|
dsURI = dsURI.replace(/%ITEM_MAXAPPVERSION%/g, aItem.maxAppVersion);
|
|
dsURI = dsURI.replace(/%APP_ID%/g, this._updater._appID);
|
|
dsURI = dsURI.replace(/%APP_VERSION%/g, this._updater._appVersion);
|
|
dsURI = dsURI.replace(/%REQ_VERSION%/g, 1);
|
|
dsURI = dsURI.replace(/%APP_OS%/g, gOSTarget);
|
|
dsURI = dsURI.replace(/%APP_ABI%/g, gXPCOMABI);
|
|
|
|
// escape() does not properly encode + symbols in any embedded FVF strings.
|
|
dsURI = dsURI.replace(/\+/g, "%2B");
|
|
|
|
// Verify that the URI provided is valid
|
|
try {
|
|
var uri = newURI(dsURI);
|
|
}
|
|
catch (e) {
|
|
LOG("RDFItemUpdater:checkForUpdates: There was an error loading the \r\n" +
|
|
" update datasource for: " + dsURI + ", item = " + aItem.id + ", error: " + e);
|
|
this._updater.checkForDone(aItem,
|
|
nsIAddonUpdateCheckListener.STATUS_FAILURE);
|
|
return;
|
|
}
|
|
|
|
LOG("RDFItemUpdater:checkForUpdates sending a request to server for: " +
|
|
uri.spec + ", item = " + aItem.objectSource);
|
|
|
|
var request = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance(Components.interfaces.nsIXMLHttpRequest);
|
|
request.open("GET", uri.spec, true);
|
|
request.overrideMimeType("text/xml");
|
|
request.setRequestHeader("Cache-Control", "no-cache");
|
|
|
|
var self = this;
|
|
request.onerror = function(event) { self.onXMLError(event, aItem); };
|
|
request.onload = function(event) { self.onXMLLoad(event, aItem); };
|
|
request.send(null);
|
|
},
|
|
|
|
onXMLLoad: function(aEvent, aItem) {
|
|
var request = aEvent.target;
|
|
var responseXML = request.responseXML;
|
|
if (responseXML)
|
|
var parseError = (responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR);
|
|
|
|
// If AMO does not return responseXML it is not treated as a failure since
|
|
// items without an updateURL are checked on AMO. If there is an XML parse
|
|
// error, responseXML is null, status does NOT equal 200 or 0 (e.g. 200 is
|
|
// HTTP OK and 0 is returned for a local file) then we don't have valid data.
|
|
if (!responseXML || parseError || (request.status != 200 && request.status != 0)) {
|
|
// If the item does not have an updateRDF then the error is from UMO.
|
|
if (!aItem.updateRDF) {
|
|
this._updater.checkForDone(aItem,
|
|
nsIAddonUpdateCheckListener.STATUS_NONE);
|
|
}
|
|
else {
|
|
this._updater.checkForDone(aItem,
|
|
nsIAddonUpdateCheckListener.STATUS_FAILURE);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var rdfParser = Components.classes["@mozilla.org/rdf/xml-parser;1"]
|
|
.createInstance(Components.interfaces.nsIRDFXMLParser)
|
|
var ds = Components.classes["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]
|
|
.createInstance(Components.interfaces.nsIRDFDataSource);
|
|
rdfParser.parseString(ds, request.channel.URI, request.responseText);
|
|
|
|
this.onDatasourceLoaded(ds, aItem);
|
|
},
|
|
|
|
/**
|
|
*
|
|
*/
|
|
onXMLError: function(aEvent, aItem) {
|
|
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(Components.interfaces.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("RDFItemUpdater:onError: There was an error loading the \r\n" +
|
|
"the update datasource for item " + aItem.id + ", error: " + statusText);
|
|
this._updater.checkForDone(aItem,
|
|
nsIAddonUpdateCheckListener.STATUS_FAILURE);
|
|
},
|
|
|
|
onDatasourceLoaded: function(aDatasource, aLocalItem) {
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// The extension update RDF file looks something like this:
|
|
//
|
|
// <RDF:Description about="urn:mozilla:extension:{GUID}">
|
|
// <em:updates>
|
|
// <RDF:Seq>
|
|
// <RDF:li resource="urn:mozilla:extension:{GUID}:4.9"/>
|
|
// <RDF:li resource="urn:mozilla:extension:{GUID}:5.0"/>
|
|
// </RDF:Seq>
|
|
// </em:updates>
|
|
// <!-- the version of the extension being offered -->
|
|
// <em:version>5.0</em:version>
|
|
// <em:updateLink>http://www.mysite.com/myext-50.xpi</em:updateLink>
|
|
// </RDF:Description>
|
|
//
|
|
// <RDF:Description about="urn:mozilla:extension:{GUID}:4.9">
|
|
// <em:version>4.9</em:version>
|
|
// <em:targetApplication>
|
|
// <RDF:Description>
|
|
// <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
|
|
// <em:minVersion>0.9</em:minVersion>
|
|
// <em:maxVersion>1.0</em:maxVersion>
|
|
// <em:updateLink>http://www.mysite.com/myext-49.xpi</em:updateLink>
|
|
// </RDF:Description>
|
|
// </em:targetApplication>
|
|
// </RDF:Description>
|
|
//
|
|
// If we get here because the following happened:
|
|
// 1) User was using Firefox 0.9 with ExtensionX 0.5 (minVersion 0.8,
|
|
// maxVersion 0.9 for Firefox)
|
|
// 2) User upgraded Firefox to 1.0
|
|
// 3) |checkForMismatches| deems ExtensionX 0.5 incompatible with this
|
|
// new version of Firefox on the basis of its maxVersion
|
|
// 4) ** We reach this point **
|
|
//
|
|
// If the version of ExtensionX (0.5) matches that provided by the
|
|
// server, then this is a cue that the author updated the rdf file
|
|
// or central repository to say "0.5 is ALSO compatible with Firefox 1.0,
|
|
// no changes are necessary." In this event, the local metadata for
|
|
// installed ExtensionX (0.5) is freshened with the new maxVersion,
|
|
// and we advance to the next item WITHOUT any download/install
|
|
// updates.
|
|
if (!aDatasource.GetAllResources().hasMoreElements()) {
|
|
LOG("RDFItemUpdater:onDatasourceLoaded: Datasource empty.\r\n" +
|
|
"If you are an Extension developer and were expecting there to be\r\n" +
|
|
"updates, this could mean any number of things, since the RDF system\r\n" +
|
|
"doesn't give up much in the way of information when the load fails.\r\n" +
|
|
"\r\nTry checking that: \r\n" +
|
|
" 1. Your remote RDF file exists at the location.\r\n" +
|
|
" 2. Your RDF file is valid XML (starts with <?xml version=\"1.0?\">\r\n" +
|
|
" and loads in Firefox displaying pretty printed like other XML documents\r\n" +
|
|
" 3. Your server is sending the data in the correct MIME\r\n" +
|
|
" type (text/xml)");
|
|
}
|
|
|
|
|
|
// Parse the response RDF
|
|
function UpdateData() {};
|
|
UpdateData.prototype = { version: "0.0", updateLink: null, updateHash: null,
|
|
minVersion: "0.0", maxVersion: "0.0" };
|
|
|
|
var versionUpdate = new UpdateData();
|
|
var newestUpdate = new UpdateData();
|
|
|
|
var newerItem, sameItem;
|
|
|
|
// Firefox 1.0PR+ update.rdf format
|
|
if (!this._versionUpdateOnly) {
|
|
// Look for newer versions of this item, we only do this in "normal"
|
|
// mode... see comment by ExtensionItemUpdater_checkForUpdates
|
|
// about how we do this in all cases but Install Phone Home - which
|
|
// only needs to do a version check.
|
|
this._parseV20UpdateInfo(aDatasource, aLocalItem, newestUpdate, false);
|
|
if (!newestUpdate.updateLink) {
|
|
// Firefox 0.9 update.rdf format - does not contain any metadata
|
|
// that can be used for version updates, so performed in the "all updates"
|
|
// mode only.
|
|
this._parseV10UpdateInfo(aDatasource, aLocalItem, newestUpdate);
|
|
}
|
|
|
|
newerItem = makeItem(aLocalItem.id,
|
|
newestUpdate.version,
|
|
aLocalItem.installLocationKey,
|
|
newestUpdate.minVersion,
|
|
newestUpdate.maxVersion,
|
|
aLocalItem.name,
|
|
newestUpdate.updateLink,
|
|
newestUpdate.updateHash,
|
|
"", /* Icon URL */
|
|
"", /* RDF Update URL */
|
|
aLocalItem.type);
|
|
if (this._updater._isValidUpdate(aLocalItem, newerItem))
|
|
++this._updater._updateCount;
|
|
else
|
|
newerItem = null;
|
|
}
|
|
|
|
// Now look for updated version compatibility metadata for the currently
|
|
// installed version...
|
|
this._parseV20UpdateInfo(aDatasource, aLocalItem, versionUpdate, true);
|
|
|
|
var result = gVersionChecker.compare(versionUpdate.version,
|
|
aLocalItem.version);
|
|
if (result == 0) {
|
|
// Local version exactly matches the "Version Update" remote version,
|
|
// Apply changes into local datasource.
|
|
sameItem = makeItem(aLocalItem.id,
|
|
versionUpdate.version,
|
|
aLocalItem.installLocationKey,
|
|
versionUpdate.minVersion,
|
|
versionUpdate.maxVersion,
|
|
aLocalItem.name,
|
|
"", /* XPI Update URL */
|
|
"", /* XPI Update Hash */
|
|
"", /* Icon URL */
|
|
"", /* RDF Update URL */
|
|
aLocalItem.type);
|
|
if (this._updater._isValidUpdate(aLocalItem, sameItem)) {
|
|
// Install-time updates are not written to the DS because there is no
|
|
// entry yet, EM just uses the notifications to ascertain (by hand)
|
|
// whether or not there is a remote maxVersion tweak that makes the
|
|
// item being installed compatible.
|
|
if (!this._updater._applyVersionUpdates(aLocalItem, sameItem))
|
|
sameItem = null;
|
|
}
|
|
else
|
|
sameItem = null;
|
|
}
|
|
|
|
if (newerItem) {
|
|
LOG("RDFItemUpdater:onDatasourceLoaded: Found a newer version of this item:\r\n" +
|
|
newerItem.objectSource);
|
|
}
|
|
if (sameItem) {
|
|
LOG("RDFItemUpdater:onDatasourceLoaded: Found info about the installed\r\n" +
|
|
"version of this item: " + sameItem.objectSource);
|
|
}
|
|
var item = null, status = nsIAddonUpdateCheckListener.STATUS_NONE;
|
|
if (!this._versionUpdateOnly && newerItem) {
|
|
item = newerItem;
|
|
status = nsIAddonUpdateCheckListener.STATUS_UPDATE;
|
|
}
|
|
else if (sameItem) {
|
|
item = sameItem;
|
|
status = nsIAddonUpdateCheckListener.STATUS_VERSIONINFO;
|
|
}
|
|
else {
|
|
item = aLocalItem;
|
|
status = nsIAddonUpdateCheckListener.STATUS_NO_UPDATE;
|
|
}
|
|
// Only one call of this._updater.checkForDone is needed for RDF
|
|
// responses, since there is only one response per item.
|
|
this._updater.checkForDone(item, status);
|
|
},
|
|
|
|
// Parses Firefox 0.9 update.rdf format
|
|
_parseV10UpdateInfo: function(aDataSource, aLocalItem, aUpdateData) {
|
|
var extensionRes = gRDF.GetResource(getItemPrefix(aLocalItem.type) + aLocalItem.id);
|
|
|
|
aUpdateData.version = this._getPropertyFromResource(aDataSource, extensionRes,
|
|
"version", aLocalItem);
|
|
aUpdateData.updateLink = this._getPropertyFromResource(aDataSource, extensionRes,
|
|
"updateLink", aLocalItem);
|
|
},
|
|
|
|
// Get a compulsory property from a resource. Reports an error if the
|
|
// property was not present.
|
|
_getPropertyFromResource: function(aDataSource, aSourceResource, aProperty, aLocalItem) {
|
|
var rv;
|
|
try {
|
|
var property = gRDF.GetResource(EM_NS(aProperty));
|
|
rv = stringData(aDataSource.GetTarget(aSourceResource, property, true));
|
|
if (rv === undefined)
|
|
throw Components.results.NS_ERROR_FAILURE;
|
|
}
|
|
catch (e) {
|
|
// XXXben show console message "aProperty" not found on aSourceResource.
|
|
return null;
|
|
}
|
|
return rv;
|
|
},
|
|
|
|
// Parses Firefox 1.0RC1+ update.rdf format
|
|
_parseV20UpdateInfo: function(aDataSource, aLocalItem, aUpdateData, aVersionUpdatesOnly) {
|
|
var extensionRes = gRDF.GetResource(getItemPrefix(aLocalItem.type) + aLocalItem.id);
|
|
|
|
var updatesArc = gRDF.GetResource(EM_NS("updates"));
|
|
var updates = aDataSource.GetTarget(extensionRes, updatesArc, true);
|
|
|
|
try {
|
|
updates = updates.QueryInterface(Components.interfaces.nsIRDFResource);
|
|
}
|
|
catch (e) {
|
|
LOG("RDFItemUpdater:_parseV20UpdateInfo: No updates were found for:\r\n" +
|
|
aLocalItem.id + "\r\n" +
|
|
"If you are an Extension developer and were expecting there to be\r\n" +
|
|
"updates, this could mean any number of things, since the RDF system\r\n" +
|
|
"doesn't give up much in the way of information when the load fails.\r\n" +
|
|
"\r\nTry checking that: \r\n" +
|
|
" 1. Your RDF File is correct - e.g. check that there is a top level\r\n" +
|
|
" RDF Resource with a URI urn:mozilla:extension:{GUID}, and that\r\n" +
|
|
" the <em:updates> listed all have matching GUIDs.");
|
|
return;
|
|
}
|
|
|
|
var cu = Components.classes["@mozilla.org/rdf/container-utils;1"]
|
|
.getService(Components.interfaces.nsIRDFContainerUtils);
|
|
if (cu.IsContainer(aDataSource, updates)) {
|
|
var ctr = getContainer(aDataSource, updates);
|
|
|
|
// In "all update types" mode, we look for newer versions, starting with the
|
|
// current installed version.
|
|
if (!aVersionUpdatesOnly)
|
|
aUpdateData.version = aLocalItem.version;
|
|
|
|
var versions = ctr.GetElements();
|
|
while (versions.hasMoreElements()) {
|
|
// There are two different methodologies for collecting version
|
|
// information depending on whether or not we've bene invoked in
|
|
// "version updates only" mode or "version+newest" mode.
|
|
var version = versions.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
this._parseV20Update(aDataSource, version, aLocalItem, aUpdateData, aVersionUpdatesOnly);
|
|
if (aVersionUpdatesOnly && aUpdateData.updateLink)
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
_parseV20Update: function(aDataSource, aUpdateResource, aLocalItem, aUpdateData, aVersionUpdatesOnly) {
|
|
var version = this._getPropertyFromResource(aDataSource, aUpdateResource,
|
|
"version", aLocalItem);
|
|
var taArc = gRDF.GetResource(EM_NS("targetApplication"));
|
|
var targetApps = aDataSource.GetTargets(aUpdateResource, taArc, true);
|
|
while (targetApps.hasMoreElements()) {
|
|
var targetApp = targetApps.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
var id = this._getPropertyFromResource(aDataSource, targetApp, "id", aLocalItem);
|
|
if (id != this._updater._appID)
|
|
continue;
|
|
|
|
var result = gVersionChecker.compare(version, aLocalItem.version);
|
|
if (aVersionUpdatesOnly ? result == 0 : result > 0) {
|
|
aUpdateData.version = version;
|
|
aUpdateData.updateLink = this._getPropertyFromResource(aDataSource, targetApp, "updateLink", aLocalItem);
|
|
aUpdateData.updateHash = this._getPropertyFromResource(aDataSource, targetApp, "updateHash", aLocalItem);
|
|
aUpdateData.minVersion = this._getPropertyFromResource(aDataSource, targetApp, "minVersion", aLocalItem);
|
|
aUpdateData.maxVersion = this._getPropertyFromResource(aDataSource, targetApp, "maxVersion", aLocalItem);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A Datasource that holds Extensions.
|
|
* - Implements nsIRDFDataSource to drive UI
|
|
* - Uses a RDF/XML datasource for storage (this is undesirable)
|
|
*
|
|
* @constructor
|
|
*/
|
|
function ExtensionsDataSource(em) {
|
|
this._em = em;
|
|
|
|
this._itemRoot = gRDF.GetResource(RDFURI_ITEM_ROOT);
|
|
this._defaultTheme = gRDF.GetResource(RDFURI_DEFAULT_THEME);
|
|
gRDF.RegisterDataSource(this, true);
|
|
}
|
|
ExtensionsDataSource.prototype = {
|
|
_inner : null,
|
|
_em : null,
|
|
_itemRoot : null,
|
|
_defaultTheme : null,
|
|
|
|
/**
|
|
* Determine if an item is compatible
|
|
* @param datasource
|
|
* The datasource to inspect for compatibility - can be the main
|
|
* datasource or an Install Manifest.
|
|
* @param source
|
|
* The RDF Resource of the item to inspect for compatibility.
|
|
* @param version
|
|
* The version of the application we are checking for compatibility
|
|
* against. If this parameter is undefined, the version of the running
|
|
* application is used.
|
|
* @returns true if the item is compatible with this version of the
|
|
* application, false, otherwise.
|
|
*/
|
|
isCompatible: function (datasource, source, version) {
|
|
// The Default Theme is always compatible.
|
|
if (source.EqualsNode(this._defaultTheme))
|
|
return true;
|
|
|
|
if (version === undefined) {
|
|
version = getPref("getCharPref", PREF_EM_APP_EXTENSIONS_VERSION,
|
|
gApp.version);
|
|
}
|
|
var appID = gApp.ID;
|
|
|
|
var targets = datasource.GetTargets(source, EM_R("targetApplication"), true);
|
|
var idRes = EM_R("id");
|
|
var minVersionRes = EM_R("minVersion");
|
|
var maxVersionRes = EM_R("maxVersion");
|
|
while (targets.hasMoreElements()) {
|
|
var targetApp = targets.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
var id = stringData(datasource.GetTarget(targetApp, idRes, true));
|
|
var minVersion = stringData(datasource.GetTarget(targetApp, minVersionRes, true));
|
|
var maxVersion = stringData(datasource.GetTarget(targetApp, maxVersionRes, true));
|
|
if (id == appID) {
|
|
var versionChecker = getVersionChecker();
|
|
return ((versionChecker.compare(version, minVersion) >= 0) &&
|
|
(versionChecker.compare(version, maxVersion) <= 0));
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Gets a list of items that are incompatible with a specific application version.
|
|
* @param appID
|
|
* The ID of the application - XXXben unused?
|
|
* @param appVersion
|
|
* The Version of the application to check for incompatibility against.
|
|
* @param desiredType
|
|
* The nsIUpdateItem type of items to look for
|
|
* @param includeDisabled
|
|
* Whether or not disabled items should be included in the set returned
|
|
* @returns An array of nsIUpdateItems that are incompatible with the application
|
|
* ID/Version supplied.
|
|
*/
|
|
getIncompatibleItemList: function(appID, appVersion, desiredType, includeDisabled) {
|
|
var items = [];
|
|
var ctr = getContainer(this._inner, this._itemRoot);
|
|
var elements = ctr.GetElements();
|
|
while (elements.hasMoreElements()) {
|
|
var item = elements.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
var id = stripPrefix(item.Value, PREFIX_ITEM_URI);
|
|
var type = this.getItemProperty(id, "type");
|
|
// Skip this item if we're not seeking disabled items
|
|
if (!includeDisabled &&
|
|
(this.getItemProperty(id, "disabled") == "true" ||
|
|
this.getItemProperty(id, "appDisabled") == "true"))
|
|
continue;
|
|
|
|
// If the id of this item matches one of the items potentially installed
|
|
// with and maintained by this application AND it is installed in the
|
|
// global install location (i.e. the place installed by the app installer)
|
|
// it is and can be managed by the update file - it's not an item that has
|
|
// been manually installed by the user into their profile dir, and as such
|
|
// it is always compatible with the next release of the application since
|
|
// we will continue to support it.
|
|
var locationKey = this.getItemProperty(id, "installLocation");
|
|
var appManaged = this.getItemProperty(id, "appManaged") == "true";
|
|
if (appManaged && locationKey == KEY_APP_GLOBAL)
|
|
continue;
|
|
|
|
if (type != -1 && (type & desiredType) &&
|
|
!this.isCompatible(this, item, appVersion))
|
|
items.push(this.getItemForID(id));
|
|
}
|
|
return items;
|
|
},
|
|
|
|
/**
|
|
* Gets a list of items of a specific type
|
|
* @param desiredType
|
|
* The nsIUpdateItem type of items to return
|
|
* @param countRef
|
|
* The XPCJS reference to the size of the returned array
|
|
* @returns An array of nsIUpdateItems, populated only with an item for |id|
|
|
* if |id| is non-null, otherwise all items matching the specified
|
|
* type.
|
|
*/
|
|
getItemList: function(desiredType, countRef) {
|
|
var items = [];
|
|
var ctr = getContainer(this, this._itemRoot);
|
|
var elements = ctr.GetElements();
|
|
while (elements.hasMoreElements()) {
|
|
var e = elements.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
var eID = stripPrefix(e.Value, PREFIX_ITEM_URI);
|
|
var type = this.getItemProperty(eID, "type");
|
|
if (type != -1 && type & desiredType)
|
|
items.push(this.getItemForID(eID));
|
|
}
|
|
countRef.value = items.length;
|
|
return items;
|
|
},
|
|
|
|
/**
|
|
* Get a list of Item IDs that have a flag set
|
|
* @param flag
|
|
* The name of an RDF property (less EM_NS) to check for
|
|
* @param desiredType
|
|
* The nsIUpdateItem type of item to look for
|
|
* @returns An array of Item IDs
|
|
*
|
|
* XXXben - this function is a little weird since it returns an array of
|
|
* strings, not an array of nsIUpdateItems...
|
|
*/
|
|
getItemsWithFlagUnset: function(flag, desiredType) {
|
|
var items = [];
|
|
|
|
var ctr = getContainer(this, this._itemRoot);
|
|
var elements = ctr.GetElements();
|
|
while (elements.hasMoreElements()) {
|
|
var e = elements.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
var id = stripPrefix(e.Value, PREFIX_ITEM_URI);
|
|
var type = this.getItemProperty(id, "type");
|
|
if (type != -1 && type & desiredType) {
|
|
var value = this.GetTarget(e, EM_R(flag), true);
|
|
if (!value)
|
|
items.push(id);
|
|
}
|
|
}
|
|
return items;
|
|
},
|
|
|
|
/**
|
|
* Constructs an nsIUpdateItem for the given item ID
|
|
* @param id
|
|
* The GUID of the item to construct a nsIUpdateItem for
|
|
* @returns The nsIUpdateItem for the id.
|
|
*/
|
|
getItemForID: function(id) {
|
|
var r = getResourceForID(id);
|
|
if (!r)
|
|
return null;
|
|
|
|
var targetAppInfo = this.getTargetApplicationInfo(id, this);
|
|
var updateHash = this.getItemProperty(id, "availableUpdateHash");
|
|
return makeItem(id,
|
|
this.getItemProperty(id, "version"),
|
|
this.getItemProperty(id, "installLocation"),
|
|
targetAppInfo ? targetAppInfo.minVersion : "",
|
|
targetAppInfo ? targetAppInfo.maxVersion : "",
|
|
this.getItemProperty(id, "name"),
|
|
this.getItemProperty(id, "availableUpdateURL"),
|
|
updateHash ? updateHash : "",
|
|
this.getItemProperty(id, "iconURL"),
|
|
this.getItemProperty(id, "updateURL"),
|
|
this.getItemProperty(id, "type"));
|
|
},
|
|
|
|
/**
|
|
* Gets the name of the Install Location where an item is installed.
|
|
* @param id
|
|
* The GUID of the item to locate an Install Location for
|
|
* @returns The string name of the Install Location where the item is
|
|
* installed.
|
|
*/
|
|
getInstallLocationKey: function(id) {
|
|
return this.getItemProperty(id, "installLocation");
|
|
},
|
|
|
|
/**
|
|
* Sets an RDF property on an item in a datasource. Does not create
|
|
* multiple assertions
|
|
* @param datasource
|
|
* The target datasource where the property should be set
|
|
* @param source
|
|
* The RDF Resource to set the property on
|
|
* @param property
|
|
* The RDF Resource of the property to set
|
|
* @param newValue
|
|
* The RDF Node containing the new property value
|
|
*/
|
|
_setProperty: function(datasource, source, property, newValue) {
|
|
var oldValue = datasource.GetTarget(source, property, true);
|
|
if (oldValue) {
|
|
if (newValue)
|
|
datasource.Change(source, property, oldValue, newValue);
|
|
else
|
|
datasource.Unassert(source, property, oldValue);
|
|
}
|
|
else if (newValue)
|
|
datasource.Assert(source, property, newValue, true);
|
|
},
|
|
|
|
/**
|
|
* Sets the target application info for an item in the Extensions
|
|
* datasource and in the item's install manifest if it is installed in a
|
|
* profile's extensions directory, it exists, and we have write access.
|
|
* @param id
|
|
* The ID of the item to update target application info for
|
|
* @param minVersion
|
|
* The minimum version of the target application that this item can
|
|
* run in
|
|
* @param maxVersion
|
|
* The maximum version of the target application that this item can
|
|
* run in
|
|
*/
|
|
updateTargetAppInfo: function(id, minVersion, maxVersion)
|
|
{
|
|
// Update the Extensions datasource
|
|
this.setTargetApplicationInfo(id, minVersion, maxVersion, null);
|
|
|
|
var installLocation = InstallLocations.get(this.getInstallLocationKey(id));
|
|
if (installLocation.name != KEY_APP_PROFILE)
|
|
return;
|
|
|
|
var installManifestFile = installLocation.getItemFile(id, FILE_INSTALL_MANIFEST);
|
|
// Only update if the item exists and we can write to the location
|
|
if (installManifestFile.exists() && installLocation.canAccess)
|
|
this.setTargetApplicationInfo(id, minVersion, maxVersion,
|
|
getInstallManifest(installManifestFile));
|
|
},
|
|
|
|
/**
|
|
* Gets the updated target application info if it exists for an item from
|
|
* the Extensions datasource during an installation or upgrade.
|
|
* @param id
|
|
* The ID of the item to discover updated target application info for
|
|
* @returns A JS Object with the following properties:
|
|
* "id" The id of the item
|
|
* "minVersion" The updated minimum version of the target
|
|
* application that this item can run in
|
|
* "maxVersion" The updated maximum version of the target
|
|
* application that this item can run in
|
|
*/
|
|
getUpdatedTargetAppInfo: function(id) {
|
|
// The default theme is always compatible so there is never update info.
|
|
if (getResourceForID(id).EqualsNode(this._defaultTheme))
|
|
return null;
|
|
|
|
var appID = gApp.ID;
|
|
var r = getResourceForID(id);
|
|
var targetApps = this._inner.GetTargets(r, EM_R("targetApplication"), true);
|
|
if (!targetApps.hasMoreElements())
|
|
targetApps = this._inner.GetTargets(gInstallManifestRoot, EM_R("targetApplication"), true);
|
|
while (targetApps.hasMoreElements()) {
|
|
var targetApp = targetApps.getNext();
|
|
if (targetApp instanceof Components.interfaces.nsIRDFResource) {
|
|
try {
|
|
var foundAppID = stringData(this._inner.GetTarget(targetApp, EM_R("id"), true));
|
|
if (foundAppID != appID) // Different target application
|
|
continue;
|
|
var updatedMinVersion = this._inner.GetTarget(targetApp, EM_R("updatedMinVersion"), true);
|
|
var updatedMaxVersion = this._inner.GetTarget(targetApp, EM_R("updatedMaxVersion"), true);
|
|
if (updatedMinVersion && updatedMaxVersion)
|
|
return { id : id,
|
|
minVersion: stringData(updatedMinVersion),
|
|
maxVersion: stringData(updatedMaxVersion) };
|
|
else
|
|
return null;
|
|
}
|
|
catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Sets the updated target application info for an item in the Extensions
|
|
* datasource during an installation or upgrade.
|
|
* @param id
|
|
* The ID of the item to set updated target application info for
|
|
* @param updatedMinVersion
|
|
* The updated minimum version of the target application that this
|
|
* item can run in
|
|
* @param updatedMaxVersion
|
|
* The updated maximum version of the target application that this
|
|
* item can run in
|
|
*/
|
|
setUpdatedTargetAppInfo: function(id, updatedMinVersion, updatedMaxVersion) {
|
|
// The default theme is always compatible so it is never updated.
|
|
if (getResourceForID(id).EqualsNode(this._defaultTheme))
|
|
return;
|
|
|
|
// Version/Dependency Info
|
|
var updatedMinVersionRes = EM_R("updatedMinVersion");
|
|
var updatedMaxVersionRes = EM_R("updatedMaxVersion");
|
|
|
|
var appID = gApp.ID;
|
|
var r = getResourceForID(id);
|
|
var targetApps = this._inner.GetTargets(r, EM_R("targetApplication"), true);
|
|
// add updatedMinVersion and updatedMaxVersion for an install else an upgrade
|
|
if (!targetApps.hasMoreElements()) {
|
|
var idRes = EM_R("id");
|
|
var targetRes = getResourceForID(id);
|
|
var property = EM_R("targetApplication");
|
|
var anon = gRDF.GetAnonymousResource();
|
|
this._inner.Assert(anon, idRes, EM_L(appID), true);
|
|
this._inner.Assert(anon, updatedMinVersionRes, EM_L(updatedMinVersion), true);
|
|
this._inner.Assert(anon, updatedMaxVersionRes, EM_L(updatedMaxVersion), true);
|
|
this._inner.Assert(targetRes, property, anon, true);
|
|
}
|
|
else {
|
|
while (targetApps.hasMoreElements()) {
|
|
var targetApp = targetApps.getNext();
|
|
if (targetApp instanceof Components.interfaces.nsIRDFResource) {
|
|
var foundAppID = stringData(this._inner.GetTarget(targetApp, EM_R("id"), true));
|
|
if (foundAppID != appID) // Different target application
|
|
continue;
|
|
this._inner.Assert(targetApp, updatedMinVersionRes, EM_L(updatedMinVersion), true);
|
|
this._inner.Assert(targetApp, updatedMaxVersionRes, EM_L(updatedMaxVersion), true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
this.Flush();
|
|
},
|
|
|
|
/**
|
|
* Gets the target application info for an item from a datasource.
|
|
* @param id
|
|
* The GUID of the item to discover target application info for
|
|
* @param datasource
|
|
* The datasource to look up target application info in
|
|
* @returns A JS Object with the following properties:
|
|
* "minVersion" The minimum version of the target application
|
|
* that this item can run in
|
|
* "maxVersion" The maximum version of the target application
|
|
* that this item can run in
|
|
* or null, if no target application data exists for the specified
|
|
* id in the supplied datasource.
|
|
*/
|
|
getTargetApplicationInfo: function(id, datasource) {
|
|
// The default theme is always compatible.
|
|
if (getResourceForID(id).EqualsNode(this._defaultTheme)) {
|
|
var ver = getPref("getCharPref", PREF_EM_APP_EXTENSIONS_VERSION,
|
|
gApp.version);
|
|
return { minVersion: ver, maxVersion: ver };
|
|
}
|
|
var appID = gApp.ID;
|
|
var r = getResourceForID(id);
|
|
var targetApps = datasource.GetTargets(r, EM_R("targetApplication"), true);
|
|
if (!targetApps)
|
|
return null;
|
|
if (!targetApps.hasMoreElements())
|
|
targetApps = datasource.GetTargets(gInstallManifestRoot, EM_R("targetApplication"), true);
|
|
while (targetApps.hasMoreElements()) {
|
|
var targetApp = targetApps.getNext();
|
|
if (targetApp instanceof Components.interfaces.nsIRDFResource) {
|
|
try {
|
|
var foundAppID = stringData(datasource.GetTarget(targetApp, EM_R("id"), true));
|
|
if (foundAppID != appID) // Different target application
|
|
continue;
|
|
|
|
return { minVersion: stringData(datasource.GetTarget(targetApp, EM_R("minVersion"), true)),
|
|
maxVersion: stringData(datasource.GetTarget(targetApp, EM_R("maxVersion"), true)) };
|
|
}
|
|
catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Sets the target application info for an item in a datasource.
|
|
* @param id
|
|
* The GUID of the item to discover target application info for
|
|
* @param minVersion
|
|
* The minimum version of the target application that this item can
|
|
* run in
|
|
* @param maxVersion
|
|
* The maximum version of the target application that this item can
|
|
* run in
|
|
* @param datasource
|
|
* The datasource to loko up target application info in
|
|
*/
|
|
setTargetApplicationInfo: function(id, minVersion, maxVersion, datasource) {
|
|
var targetDataSource = datasource;
|
|
if (!targetDataSource)
|
|
targetDataSource = this._inner;
|
|
|
|
var appID = gApp.ID;
|
|
var r = getResourceForID(id);
|
|
var targetApps = targetDataSource.GetTargets(r, EM_R("targetApplication"), true);
|
|
if (!targetApps.hasMoreElements())
|
|
targetApps = datasource.GetTargets(gInstallManifestRoot, EM_R("targetApplication"), true);
|
|
while (targetApps.hasMoreElements()) {
|
|
var targetApp = targetApps.getNext();
|
|
if (targetApp instanceof Components.interfaces.nsIRDFResource) {
|
|
var foundAppID = stringData(targetDataSource.GetTarget(targetApp, EM_R("id"), true));
|
|
if (foundAppID != appID) // Different target application
|
|
continue;
|
|
|
|
this._setProperty(targetDataSource, targetApp, EM_R("minVersion"), EM_L(minVersion));
|
|
this._setProperty(targetDataSource, targetApp, EM_R("maxVersion"), EM_L(maxVersion));
|
|
|
|
// If we were setting these properties on the main datasource, flush
|
|
// it now. (Don't flush changes set on Install Manifests - they are
|
|
// fleeting).
|
|
if (!datasource)
|
|
this.Flush();
|
|
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Gets a property of an item
|
|
* @param id
|
|
* The GUID of the item
|
|
* @param property
|
|
* The name of the property (excluding EM_NS)
|
|
* @returns The literal value of the property, or undefined if there is no
|
|
* value.
|
|
*/
|
|
getItemProperty: function(id, property) {
|
|
var item = getResourceForID(id);
|
|
if (!item) {
|
|
LOG("getItemProperty failing for lack of an item. This means getResourceForItem \
|
|
failed to locate a resource for aItemID (item ID = " + id + ", property = " + property + ")");
|
|
}
|
|
else
|
|
return this._getItemProperty(item, property);
|
|
return undefined;
|
|
},
|
|
|
|
/**
|
|
* Gets a property of an item resource
|
|
* @param itemResource
|
|
* The RDF Resource of the item
|
|
* @param property
|
|
* The name of the property (excluding EM_NS)
|
|
* @returns The literal value of the property, or undefined if there is no
|
|
* value.
|
|
*/
|
|
_getItemProperty: function(itemResource, property) {
|
|
var target = this.GetTarget(itemResource, EM_R(property), true);
|
|
var value = stringData(target);
|
|
if (value === undefined)
|
|
value = intData(target);
|
|
return value === undefined ? "" : value;
|
|
},
|
|
|
|
/**
|
|
* Sets a property on an item.
|
|
* @param id
|
|
* The GUID of the item
|
|
* @param propertyArc
|
|
* The RDF Resource of the property arc
|
|
* @param propertyValue
|
|
* A nsIRDFLiteral value of the property to be set
|
|
*/
|
|
setItemProperty: function (id, propertyArc, propertyValue) {
|
|
var item = getResourceForID(id);
|
|
this._setProperty(this._inner, item, propertyArc, propertyValue);
|
|
this.Flush();
|
|
},
|
|
|
|
/**
|
|
* Inserts the RDF resource for an item into a container.
|
|
* @param id
|
|
* The GUID of the item
|
|
*/
|
|
insertItemIntoContainer: function(id) {
|
|
// Get the target container and resource
|
|
var ctr = getContainer(this._inner, this._itemRoot);
|
|
var itemResource = getResourceForID(id);
|
|
// Don't bother adding the extension to the list if it's already there.
|
|
// (i.e. we're upgrading)
|
|
var oldIndex = ctr.IndexOf(itemResource);
|
|
if (oldIndex == -1)
|
|
ctr.AppendElement(itemResource);
|
|
this.Flush();
|
|
},
|
|
|
|
/**
|
|
* Removes the RDF resource for an item from its container.
|
|
* @param id
|
|
* The GUID of the item
|
|
*/
|
|
removeItemFromContainer: function(id) {
|
|
var ctr = getContainer(this._inner, this._itemRoot);
|
|
var itemResource = getResourceForID(id);
|
|
ctr.RemoveElement(itemResource, true);
|
|
this.Flush();
|
|
},
|
|
|
|
/**
|
|
* Removes a corrupt item entry from the extension list added due to buggy
|
|
* code in previous EM versions!
|
|
* @param id
|
|
* The GUID of the item
|
|
*/
|
|
removeCorruptItem: function(id) {
|
|
this.removeItemMetadata(id);
|
|
this.removeItemFromContainer(id);
|
|
},
|
|
|
|
/**
|
|
* Removes a corrupt download entry from the list
|
|
* @param uri
|
|
* The RDF URI of the item.
|
|
* @returns The RDF Resource of the removed entry
|
|
*/
|
|
removeCorruptDLItem: function(uri) {
|
|
var itemResource = gRDF.GetResource(uri);
|
|
var ctr = getContainer(this._inner, this._itemRoot);
|
|
if (ctr.IndexOf(itemResource) != -1) {
|
|
ctr.RemoveElement(itemResource, true);
|
|
this._cleanResource(itemResource);
|
|
this.Flush();
|
|
}
|
|
return itemResource;
|
|
},
|
|
|
|
/**
|
|
* Copies metadata from an Install Manifest Datasource into the Extensions
|
|
* DataSource.
|
|
* @param id
|
|
* The GUID of the item
|
|
* @param installManifest
|
|
* The Install Manifest datasource we are copying from
|
|
* @param installLocation
|
|
* The Install Location of the item.
|
|
*/
|
|
addItemMetadata: function(id, installManifest, installLocation) {
|
|
// Copy the assertions over from the source datasource.
|
|
var targetRes = getResourceForID(id);
|
|
// Assert properties with single values
|
|
var singleProps = ["version", "name", "description", "creator", "homepageURL",
|
|
"updateURL", "updateService", "optionsURL", "aboutURL",
|
|
"iconURL", "internalName"];
|
|
|
|
// Items installed into restricted Install Locations can also be locked
|
|
// (can't be removed or disabled), and hidden (not shown in the UI)
|
|
if (installLocation.restricted)
|
|
singleProps = singleProps.concat(["locked", "hidden"]);
|
|
if (installLocation.name == KEY_APP_GLOBAL)
|
|
singleProps = singleProps.concat(["appManaged"]);
|
|
for (var i = 0; i < singleProps.length; ++i) {
|
|
var property = EM_R(singleProps[i]);
|
|
var literal = installManifest.GetTarget(gInstallManifestRoot, property, true);
|
|
// If literal is null, _setProperty will remove any existing.
|
|
this._setProperty(this._inner, targetRes, property, literal);
|
|
}
|
|
|
|
// Assert properties with multiple values
|
|
var manyProps = ["contributor"];
|
|
for (var i = 0; i < manyProps.length; ++i) {
|
|
var property = EM_R(manyProps[i]);
|
|
var literals = installManifest.GetTargets(gInstallManifestRoot, property, true);
|
|
|
|
var oldValues = this._inner.GetTargets(targetRes, property, true);
|
|
while (oldValues.hasMoreElements()) {
|
|
var oldValue = oldValues.getNext().QueryInterface(Components.interfaces.nsIRDFNode);
|
|
this._inner.Unassert(targetRes, property, oldValue);
|
|
}
|
|
while (literals.hasMoreElements()) {
|
|
var literal = literals.getNext().QueryInterface(Components.interfaces.nsIRDFNode);
|
|
this._inner.Assert(targetRes, property, literal, true);
|
|
}
|
|
}
|
|
|
|
// Version/Dependency Info
|
|
var versionProps = ["targetApplication", "requires"];
|
|
var idRes = EM_R("id");
|
|
var minVersionRes = EM_R("minVersion");
|
|
var maxVersionRes = EM_R("maxVersion");
|
|
for (var i = 0; i < versionProps.length; ++i) {
|
|
var property = EM_R(versionProps[i]);
|
|
var newVersionInfos = installManifest.GetTargets(gInstallManifestRoot, property, true);
|
|
|
|
var oldVersionInfos = this._inner.GetTargets(targetRes, property, true);
|
|
while (oldVersionInfos.hasMoreElements()) {
|
|
var oldVersionInfo = oldVersionInfos.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
this._cleanResource(oldVersionInfo);
|
|
this._inner.Unassert(targetRes, property, oldVersionInfo);
|
|
}
|
|
while (newVersionInfos.hasMoreElements()) {
|
|
var newVersionInfo = newVersionInfos.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
var anon = gRDF.GetAnonymousResource();
|
|
this._inner.Assert(anon, idRes, installManifest.GetTarget(newVersionInfo, idRes, true), true);
|
|
this._inner.Assert(anon, minVersionRes, installManifest.GetTarget(newVersionInfo, minVersionRes, true), true);
|
|
this._inner.Assert(anon, maxVersionRes, installManifest.GetTarget(newVersionInfo, maxVersionRes, true), true);
|
|
this._inner.Assert(targetRes, property, anon, true);
|
|
}
|
|
}
|
|
this.updateProperty(id, "opType");
|
|
this.updateProperty(id, "updateable");
|
|
this.updateProperty(id, "displayDescription");
|
|
this.Flush();
|
|
},
|
|
|
|
/**
|
|
* Strips an item entry of all assertions.
|
|
* @param id
|
|
* The GUID of the item
|
|
*/
|
|
removeItemMetadata: function(id) {
|
|
var item = getResourceForID(id);
|
|
var resources = ["targetApplication", "requires"];
|
|
for (var i = 0; i < resources.length; ++i) {
|
|
var targetApps = this._inner.GetTargets(item, EM_R(resources[i]), true);
|
|
while (targetApps.hasMoreElements()) {
|
|
var targetApp = targetApps.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
this._cleanResource(targetApp);
|
|
}
|
|
}
|
|
|
|
this._cleanResource(item);
|
|
},
|
|
|
|
/**
|
|
* Strips a resource of all outbound assertions. We use methods like this
|
|
* since the RDFXMLDatasource will write out all assertions, even if they
|
|
* are not connected through our root.
|
|
* @param resource
|
|
* The resource to clean.
|
|
*/
|
|
_cleanResource: function(resource) {
|
|
// Remove outward arcs
|
|
var arcs = this._inner.ArcLabelsOut(resource);
|
|
while (arcs.hasMoreElements()) {
|
|
var arc = arcs.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
var targets = this._inner.GetTargets(resource, arc, true);
|
|
while (targets.hasMoreElements()) {
|
|
var value = targets.getNext().QueryInterface(Components.interfaces.nsIRDFNode);
|
|
if (value)
|
|
this._inner.Unassert(resource, arc, value);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Notify views that this propery has changed (this is for properties that
|
|
* are implemented by this datasource rather than by the inner in-memory
|
|
* datasource and thus do not get free change handling).
|
|
* @param id
|
|
* The GUID of the item to update the property for.
|
|
* @param property
|
|
* The property (less EM_NS) to update.
|
|
*/
|
|
updateProperty: function(id, property) {
|
|
var item = getResourceForID(id);
|
|
var propertyResource = EM_R(property);
|
|
var value = this.GetTarget(item, propertyResource, true);
|
|
if (item && value) {
|
|
for (var i = 0; i < this._observers.length; ++i)
|
|
this._observers[i].onChange(this, item, propertyResource,
|
|
EM_L(""), value);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Move an Item to the index of another item in its container.
|
|
* @param movingID
|
|
* The ID of the item to be moved.
|
|
* @param destinationID
|
|
* The ID of an item to move another item to.
|
|
*/
|
|
moveToIndexOf: function(movingID, destinationID) {
|
|
var extensions = gRDF.GetResource(RDFURI_ITEM_ROOT);
|
|
var ctr = getContainer(this._inner, extensions);
|
|
var item = gRDF.GetResource(movingID);
|
|
var index = ctr.IndexOf(gRDF.GetResource(destinationID));
|
|
if (index == -1)
|
|
index = 1; // move to the beginning if destinationID is not found
|
|
this._inner.beginUpdateBatch();
|
|
ctr.RemoveElement(item, true);
|
|
ctr.InsertElementAt(item, index, true);
|
|
this._inner.endUpdateBatch();
|
|
this.Flush();
|
|
},
|
|
|
|
/**
|
|
* Determines if an Item is an active download
|
|
* @param id
|
|
* The GUID of the item
|
|
* @returns true if the item is an active download, false otherwise.
|
|
*/
|
|
isDownloadItem: function(id) {
|
|
return this.getItemProperty(id, "downloadURL") != "";
|
|
},
|
|
|
|
/**
|
|
* Adds an entry representing an active download to the appropriate container
|
|
* @param addon
|
|
* An object implementing nsIUpdateItem for the addon being
|
|
* downloaded.
|
|
*/
|
|
addDownload: function(addon) {
|
|
if (addon.id != addon.xpiURL)
|
|
return;
|
|
var res = gRDF.GetResource(addon.xpiURL);
|
|
this._setProperty(this._inner, res, EM_R("name"), EM_L(addon.name));
|
|
this._setProperty(this._inner, res, EM_R("version"), EM_L(addon.version));
|
|
this._setProperty(this._inner, res, EM_R("iconURL"), EM_L(addon.iconURL));
|
|
this._setProperty(this._inner, res, EM_R("downloadURL"), EM_L(addon.xpiURL));
|
|
this._setProperty(this._inner, res, EM_R("type"), EM_I(addon.type));
|
|
|
|
var ctr = getContainer(this._inner, this._itemRoot);
|
|
if (ctr.IndexOf(res) == -1)
|
|
ctr.AppendElement(res);
|
|
|
|
this.Flush();
|
|
},
|
|
|
|
/**
|
|
* Adds an entry representing an item that is incompatible and is being
|
|
* checked for a compatibility update.
|
|
* @param name
|
|
* The display name of the item being checked
|
|
* @param url
|
|
* The URL string of the xpi file that has been staged. This is
|
|
* also used for installLocation to make this an independently
|
|
* managed item
|
|
* @param type
|
|
* The nsIUpdateItem type of the item
|
|
* @param version
|
|
* The version of the item
|
|
*/
|
|
addIncompatibleUpdateItem: function(name, url, type, version) {
|
|
// type must be TYPE_EXTENSION for a multi_xpi to display in the manager.
|
|
if (type == nsIUpdateItem.TYPE_MULTI_XPI)
|
|
type = nsIUpdateItem.TYPE_EXTENSION;
|
|
|
|
var iconURL = (type == nsIUpdateItem.TYPE_THEME) ? URI_GENERIC_ICON_THEME :
|
|
URI_GENERIC_ICON_XPINSTALL;
|
|
var extensionsStrings = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
|
|
var updateMsg = extensionsStrings.formatStringFromName("incompatibleUpdateMessage",
|
|
[BundleManager.appName, name], 2)
|
|
|
|
var res = gRDF.GetResource(url);
|
|
this._setProperty(this._inner, res, EM_R("name"), EM_L(name));
|
|
this._setProperty(this._inner, res, EM_R("iconURL"), EM_L(iconURL));
|
|
this._setProperty(this._inner, res, EM_R("downloadURL"), EM_L(url));
|
|
this._setProperty(this._inner, res, EM_R("type"), EM_I(type));
|
|
this._setProperty(this._inner, res, EM_R("version"), EM_L(version));
|
|
this._setProperty(this._inner, res, EM_R("incompatibleUpdate"), EM_L("true"));
|
|
this._setProperty(this._inner, res, EM_R("description"), EM_L(updateMsg));
|
|
|
|
var ctr = getContainer(this._inner, this._itemRoot);
|
|
if (ctr.IndexOf(res) == -1)
|
|
ctr.AppendElement(res);
|
|
|
|
this.Flush();
|
|
},
|
|
|
|
/**
|
|
* Removes an active download from the appropriate container
|
|
* @param url
|
|
* The URL string of the active download to be removed
|
|
*/
|
|
removeDownload: function(url) {
|
|
var res = gRDF.GetResource(url);
|
|
var ctr = getContainer(this._inner, this._itemRoot);
|
|
if (ctr.IndexOf(res) != -1)
|
|
ctr.RemoveElement(res, true);
|
|
this._cleanResource(res);
|
|
this.Flush();
|
|
},
|
|
|
|
/**
|
|
* Write download progress info for a set of items to the Datasource
|
|
* @param data
|
|
* A JS Object containing progress information for a set of active
|
|
* downloads, hashed by URL. Each object has the following properties:
|
|
* "state" An integer value representing the download/install
|
|
* state.
|
|
* "progress" An integer value between 0 and 100 representing
|
|
* percentage complete
|
|
*/
|
|
flushProgressInfo: function(data) {
|
|
for (var url in data) {
|
|
var res = gRDF.GetResource(url);
|
|
this._setProperty(this._inner, res, EM_R("state"), EM_I(data[url].state));
|
|
this._setProperty(this._inner, res, EM_R("progress"), EM_I(data[url].progress));
|
|
}
|
|
this.Flush();
|
|
},
|
|
|
|
/**
|
|
* A GUID->location-key hash of items that are visible to the application.
|
|
* These are items that show up in the Extension/Themes etc UI. If there is
|
|
* an instance of the same item installed in Install Locations of differing
|
|
* profiles, the item at the highest priority location will appear in this
|
|
* list.
|
|
*/
|
|
visibleItems: { },
|
|
|
|
/**
|
|
* Walk the list of installed items and determine what the visible list is,
|
|
* based on which items are visible at the highest priority locations.
|
|
*/
|
|
_buildVisibleItemList: function() {
|
|
var ctr = getContainer(this, this._itemRoot);
|
|
var items = ctr.GetElements();
|
|
while (items.hasMoreElements()) {
|
|
var item = items.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
|
|
// Resource URIs adopt the format: location-key,item-id
|
|
var id = stripPrefix(item.Value, PREFIX_ITEM_URI);
|
|
this.visibleItems[id] = this.getItemProperty(id, "installLocation");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Updates an item's location in the visible item list.
|
|
* @param id
|
|
* The GUID of the item to update
|
|
* @param locationKey
|
|
* The name of the Install Location where the item is installed.
|
|
* @param forceReplace
|
|
* true if the new location should be used, regardless of its
|
|
* priority relationship to existing entries, false if the location
|
|
* should only be updated if its priority is lower than the existing
|
|
* value.
|
|
*/
|
|
updateVisibleList: function(id, locationKey, forceReplace) {
|
|
if (id in this.visibleItems && this.visibleItems[id]) {
|
|
var oldLocation = InstallLocations.get(this.visibleItems[id]);
|
|
var newLocation = InstallLocations.get(locationKey);
|
|
if (forceReplace || newLocation.priority < oldLocation.priority)
|
|
this.visibleItems[id] = locationKey;
|
|
}
|
|
else
|
|
this.visibleItems[id] = locationKey;
|
|
},
|
|
|
|
/**
|
|
* Load the Extensions Datasource from disk.
|
|
*/
|
|
loadExtensions: function() {
|
|
var extensionsFile = getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS]);
|
|
this._inner = gRDF.GetDataSourceBlocking(getURLSpecFromFile(extensionsFile));
|
|
|
|
var cu = Components.classes["@mozilla.org/rdf/container-utils;1"]
|
|
.getService(Components.interfaces.nsIRDFContainerUtils);
|
|
cu.MakeSeq(this._inner, this._itemRoot);
|
|
|
|
this._buildVisibleItemList();
|
|
},
|
|
|
|
/**
|
|
* A hash of Addon IDs that we are currently looking for updates to.
|
|
*/
|
|
_updateURLs: { },
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
onUpdateStarted: function() {
|
|
LOG("Datasource: Update Started");
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
onUpdateEnded: function() {
|
|
LOG("Datasource: Update Ended");
|
|
this._updateURLs = { };
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
onAddonUpdateStarted: function(addon) {
|
|
LOG("Datasource: Addon Update Started: " + addon.id);
|
|
this._updateURLs[addon.id] = addon.id;
|
|
this.updateProperty(addon.id, "availableUpdateURL");
|
|
this.updateProperty(addon.id, "displayDescription");
|
|
},
|
|
|
|
/**
|
|
* See nsIExtensionManager.idl
|
|
*/
|
|
onAddonUpdateEnded: function(addon, status) {
|
|
LOG("Datasource: Addon Update Ended: " + addon.id + ", status: " + status);
|
|
this._updateURLs[addon.id] = status;
|
|
var url = null, hash = null, version = null;
|
|
var updateAvailable = status == nsIAddonUpdateCheckListener.STATUS_UPDATE;
|
|
if (updateAvailable) {
|
|
url = EM_L(addon.xpiURL);
|
|
if (addon.xpiHash)
|
|
hash = EM_L(addon.xpiHash);
|
|
version = EM_L(addon.version);
|
|
}
|
|
this.setItemProperty(addon.id, EM_R("availableUpdateURL"), url);
|
|
this.setItemProperty(addon.id, EM_R("availableUpdateHash"), hash);
|
|
this.setItemProperty(addon.id, EM_R("availableUpdateVersion"), version);
|
|
this.updateProperty(addon.id, "availableUpdateURL");
|
|
this.updateProperty(addon.id, "displayDescription");
|
|
},
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// nsIRDFDataSource
|
|
get URI() {
|
|
return "rdf:extensions";
|
|
},
|
|
|
|
GetSource: function(property, target, truthValue) {
|
|
return this._inner.GetSource(property, target, truthValue);
|
|
},
|
|
|
|
GetSources: function(property, target, truthValue) {
|
|
return this._inner.GetSources(property, target, truthValue);
|
|
},
|
|
|
|
/**
|
|
* Gets an URL to a theme's image file
|
|
* @param item
|
|
* The RDF Resource representing the item
|
|
* @param fileName
|
|
* The file to locate a URL for
|
|
* @param fallbackURL
|
|
* If the location fails, supply this URL instead
|
|
* @returns An RDF Resource to the URL discovered, or the fallback
|
|
* if the discovery failed.
|
|
*/
|
|
_getThemeImageURL: function(item, fileName, fallbackURL) {
|
|
var id = stripPrefix(item.Value, PREFIX_ITEM_URI);
|
|
var installLocation = this._em.getInstallLocation(id);
|
|
var file = installLocation.getItemFile(id, fileName)
|
|
if (file.exists())
|
|
return gRDF.GetResource(getURLSpecFromFile(file));
|
|
|
|
if (id == stripPrefix(RDFURI_DEFAULT_THEME, PREFIX_ITEM_URI)) {
|
|
var jarFile = getFile(KEY_APPDIR, [DIR_CHROME, FILE_DEFAULT_THEME_JAR]);
|
|
var url = "jar:" + getURLSpecFromFile(jarFile) + "!/" + fileName;
|
|
return gRDF.GetResource(url);
|
|
}
|
|
|
|
return fallbackURL ? gRDF.GetResource(fallbackURL) : null;
|
|
},
|
|
|
|
/**
|
|
* Get the em:iconURL property (icon url of the item)
|
|
*/
|
|
_rdfGet_iconURL: function(item, property) {
|
|
var id = stripPrefix(item.Value, PREFIX_ITEM_URI);
|
|
var type = this.getItemProperty(id, "type");
|
|
if (type != -1 && type & nsIUpdateItem.TYPE_EXTENSION) {
|
|
var hasIconURL = this._inner.hasArcOut(item, property);
|
|
// If the download entry doesn't have a IconURL property, use a
|
|
// generic icon URL instead.
|
|
if (!hasIconURL || this.getItemProperty(id, "disabled") == "true")
|
|
return gRDF.GetResource(URI_GENERIC_ICON_XPINSTALL);
|
|
var iconURL = this._inner.GetTarget(item, property, true);
|
|
iconURL = iconURL.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
|
|
var uri = newURI(iconURL);
|
|
try {
|
|
var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
|
|
.getService(Components.interfaces.nsIChromeRegistry);
|
|
cr.convertChromeURL(uri);
|
|
}
|
|
catch(e) {
|
|
// bogus URI, supply a generic icon.
|
|
return gRDF.GetResource(URI_GENERIC_ICON_XPINSTALL);
|
|
}
|
|
}
|
|
else if (type != -1 && type & nsIUpdateItem.TYPE_THEME)
|
|
return this._getThemeImageURL(item, "icon.png", URI_GENERIC_ICON_THEME);
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Get the em:previewImage property (preview image of the item)
|
|
*/
|
|
_rdfGet_previewImage: function(item, property) {
|
|
var type = this.getItemProperty(stripPrefix(item.Value, PREFIX_ITEM_URI), "type");
|
|
if (type != -1 && type & nsIUpdateItem.TYPE_THEME)
|
|
return this._getThemeImageURL(item, "preview.png", null);
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* If we're in safe mode, the item is disabled by the user or app, or the
|
|
* item is to be upgraded force the generic about dialog for the item.
|
|
*/
|
|
_rdfGet_aboutURL: function(item, property) {
|
|
var id = stripPrefix(item.Value, PREFIX_ITEM_URI);
|
|
if (inSafeMode() || this.getItemProperty(id, "disabled") == "true" ||
|
|
this.getItemProperty(id, "opType") == OP_NEEDS_UPGRADE)
|
|
return EM_L("");
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Get the em:compatible property (whether or not this item is compatible)
|
|
*/
|
|
_rdfGet_compatible: function(item, property) {
|
|
var id = stripPrefix(item.Value, PREFIX_ITEM_URI);
|
|
var targetAppInfo = this.getTargetApplicationInfo(id, this);
|
|
if (!targetAppInfo)
|
|
return EM_L("false");
|
|
getVersionChecker();
|
|
|
|
var appVersion = getPref("getCharPref", PREF_EM_APP_EXTENSIONS_VERSION,
|
|
gApp.version);
|
|
if (gVersionChecker.compare(targetAppInfo.maxVersion, appVersion) < 0 ||
|
|
gVersionChecker.compare(appVersion, targetAppInfo.minVersion) < 0) {
|
|
// OK, this item is incompatible.
|
|
return EM_L("false");
|
|
}
|
|
return EM_L("true");
|
|
},
|
|
|
|
/**
|
|
* Gets the em:availableUpdateURL - the URL to an XPI update package, if
|
|
* present, or a literal string "novalue" if there is no update XPI URL.
|
|
*/
|
|
_rdfGet_availableUpdateURL: function(item, property) {
|
|
var value = this._inner.GetTarget(item, property, true);
|
|
if (!value)
|
|
return EM_L("none");
|
|
return value;
|
|
},
|
|
|
|
/**
|
|
* Get the em:displayDescription property (description that is displayed
|
|
* in the UI - not always the info description of the item - sometimes
|
|
* a message like "will be uninstalled after restart" etc)
|
|
*/
|
|
_rdfGet_displayDescription: function(item, property) {
|
|
var id = stripPrefix(item.Value, PREFIX_ITEM_URI);
|
|
|
|
var extensionsStrings = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES);
|
|
var itemName = this.getItemProperty(id, "name");
|
|
var opType = this.getItemProperty(id, "opType");
|
|
|
|
function getLiteral(key, strings) {
|
|
return EM_L(extensionsStrings.formatStringFromName(key,
|
|
strings, strings.length));
|
|
}
|
|
if (id in this._updateURLs && this._updateURLs[id]) {
|
|
switch (this._updateURLs[id]) {
|
|
case id:
|
|
return getLiteral("updatingMessage", [itemName]);
|
|
case nsIAddonUpdateCheckListener.STATUS_APP_MANAGED:
|
|
case nsIAddonUpdateCheckListener.STATUS_NO_UPDATE:
|
|
return getLiteral("updateNoUpdateMessage", [itemName]);
|
|
case nsIAddonUpdateCheckListener.STATUS_VERSIONINFO:
|
|
if (opType == OP_NEEDS_UPGRADE || opType == OP_NEEDS_INSTALL)
|
|
break;
|
|
return getLiteral("updateCompatibilityMessage", [itemName]);
|
|
case nsIAddonUpdateCheckListener.STATUS_FAILURE:
|
|
return getLiteral("updateErrorMessage", [itemName]);
|
|
case nsIAddonUpdateCheckListener.STATUS_DISABLED:
|
|
return getLiteral("updateDisabledMessage", [itemName]);
|
|
case nsIAddonUpdateCheckListener.STATUS_READ_ONLY:
|
|
return getLiteral("updateReadOnlyMessage", []);
|
|
case nsIAddonUpdateCheckListener.STATUS_NOT_MANAGED:
|
|
return getLiteral("updateNotManagedMessage", [BundleManager.appName]);
|
|
}
|
|
}
|
|
var node = this._inner.GetTarget(item, EM_R("availableUpdateURL"), true);
|
|
if (node) {
|
|
var version = this.getItemProperty(id, "availableUpdateVersion");
|
|
return getLiteral("updateAvailableMessage", [itemName, version]);
|
|
}
|
|
switch (opType) {
|
|
case OP_NEEDS_DISABLE:
|
|
return getLiteral("restartBeforeDisableMessage", [itemName, BundleManager.appName]);
|
|
case OP_NEEDS_ENABLE:
|
|
return getLiteral("restartBeforeEnableMessage", [itemName, BundleManager.appName]);
|
|
case OP_NEEDS_INSTALL:
|
|
return getLiteral("restartBeforeInstallMessage", [itemName, BundleManager.appName]);
|
|
case OP_NEEDS_UNINSTALL:
|
|
return getLiteral("restartBeforeUninstallMessage", [itemName, BundleManager.appName]);
|
|
case OP_NEEDS_UPGRADE:
|
|
return getLiteral("restartBeforeUpgradeMessage", [itemName, BundleManager.appName]);
|
|
}
|
|
|
|
if (this.getItemProperty(id, "appDisabled") == "true" &&
|
|
this.getItemProperty(id, "compatible") != "true")
|
|
return getLiteral("incompatibleExtension", [BundleManager.appName, gApp.version]);
|
|
|
|
if (inSafeMode())
|
|
return getLiteral("disabledBySafeMode", [itemName, BundleManager.appName]);
|
|
|
|
// No special state for this item, so just use the "description" property.
|
|
return this.GetTarget(item, EM_R("description"), true);
|
|
},
|
|
|
|
/**
|
|
* Get the em:opType property (controls widget state for the EM UI)
|
|
* from the Startup Cache (e.g. extensions.cache)
|
|
*/
|
|
_rdfGet_opType: function(item, property) {
|
|
var id = stripPrefix(item.Value, PREFIX_ITEM_URI);
|
|
var key = this.getItemProperty(id, "installLocation");
|
|
if (key in StartupCache.entries && id in StartupCache.entries[key] &&
|
|
StartupCache.entries[key][id])
|
|
return EM_L(StartupCache.entries[key][id].op);
|
|
return EM_L(OP_NONE);
|
|
},
|
|
|
|
/**
|
|
* Gets a localizable property. Install Manifests are generally only in one
|
|
* language, however an item can customize by providing localized prefs in
|
|
* the form:
|
|
*
|
|
* extensions.{GUID}.[name|description|creator|homepageURL]
|
|
*
|
|
* to specify localized text for each of these properties.
|
|
*/
|
|
_getLocalizablePropertyValue: function(item, property) {
|
|
// These are localizable properties that a language pack supplied by the
|
|
// Extension may override.
|
|
var prefName = PREF_EM_EXTENSION_FORMAT.replace(/%UUID%/,
|
|
stripPrefix(item.Value, PREFIX_ITEM_URI)) +
|
|
stripPrefix(property.Value, PREFIX_NS_EM);
|
|
try {
|
|
var value = gPref.getComplexValue(prefName,
|
|
Components.interfaces.nsIPrefLocalizedString);
|
|
if (value.data)
|
|
return EM_L(value.data);
|
|
}
|
|
catch (e) {
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Get the em:name property (name of the item)
|
|
*/
|
|
_rdfGet_name: function(item, property) {
|
|
return this._getLocalizablePropertyValue(item, property);
|
|
},
|
|
|
|
/**
|
|
* Get the em:description property (description of the item)
|
|
*/
|
|
_rdfGet_description: function(item, property) {
|
|
return this._getLocalizablePropertyValue(item, property);
|
|
},
|
|
|
|
/**
|
|
* Get the em:creator property (creator of the item)
|
|
*/
|
|
_rdfGet_creator: function(item, property) {
|
|
return this._getLocalizablePropertyValue(item, property);
|
|
},
|
|
|
|
/**
|
|
* Get the em:homepageURL property (homepage URL of the item)
|
|
*/
|
|
_rdfGet_homepageURL: function(item, property) {
|
|
return this._getLocalizablePropertyValue(item, property);
|
|
},
|
|
|
|
/**
|
|
* Get the em:disabled property. This will be true if the item has a
|
|
* appDisabled or a userDisabled property that is true as long as it is not
|
|
* about to be disabled.
|
|
*/
|
|
_rdfGet_disabled: function(item, property) {
|
|
var id = stripPrefix(item.Value, PREFIX_ITEM_URI);
|
|
if ((this.getItemProperty(id, "userDisabled") == "true" ||
|
|
this.getItemProperty(id, "appDisabled") == "true") &&
|
|
this.getItemProperty(id, "opType") != OP_NEEDS_DISABLE)
|
|
return EM_L("true");
|
|
|
|
// Migrate disabled in the extensions datasource to userDisabled or
|
|
// appDisabled as appropriate.
|
|
var oldDisabled = this._inner.GetTarget(item, property, true);
|
|
if (oldDisabled instanceof Components.interfaces.nsIRDFLiteral) {
|
|
this._inner.Unassert(item, property, oldDisabled);
|
|
if (this.getItemProperty(id, "compatible") == "true")
|
|
this.setItemProperty(id, EM_R("userDisabled"), EM_L("true"));
|
|
else
|
|
this.setItemProperty(id, EM_R("appDisabled"), EM_L("true"));
|
|
|
|
return EM_L("true");
|
|
}
|
|
|
|
return EM_L("false");
|
|
},
|
|
|
|
/**
|
|
* Get the em:updateable property - this specifies whether the item is
|
|
* allowed to be updated
|
|
*/
|
|
_rdfGet_updateable: function(item, property) {
|
|
var id = stripPrefix(item.Value, PREFIX_ITEM_URI);
|
|
var opType = this.getItemProperty(id, "opType");
|
|
if (opType == OP_NEEDS_INSTALL || opType == OP_NEEDS_UNINSTALL ||
|
|
opType == OP_NEEDS_UPGRADE ||
|
|
this.getItemProperty(id, "appManaged") == "true")
|
|
return EM_L("false");
|
|
|
|
try {
|
|
if (!gPref.getBoolPref(PREF_EM_ITEM_UPDATE_ENABLED.replace(/%UUID%/, id)))
|
|
return EM_L("false");
|
|
}
|
|
catch (e) { }
|
|
|
|
var installLocation = InstallLocations.get(this.getInstallLocationKey(id));
|
|
if (!installLocation || !installLocation.canAccess)
|
|
return EM_L("false");
|
|
|
|
return EM_L("true");
|
|
},
|
|
|
|
/**
|
|
* See nsIRDFDataSource.idl
|
|
*/
|
|
GetTarget: function(source, property, truthValue) {
|
|
if (!source)
|
|
return null;
|
|
|
|
var target = null;
|
|
var getter = "_rdfGet_" + stripPrefix(property.Value, PREFIX_NS_EM);
|
|
if (getter in this)
|
|
target = this[getter](source, property);
|
|
|
|
return target || this._inner.GetTarget(source, property, truthValue);
|
|
},
|
|
|
|
/**
|
|
* Gets an enumeration of values of a localizable property. Install Manifests
|
|
* are generally only in one language, however an item can customize by
|
|
* providing localized prefs in the form:
|
|
*
|
|
* extensions.{GUID}.[contributor].1
|
|
* extensions.{GUID}.[contributor].2
|
|
* extensions.{GUID}.[contributor].3
|
|
* ...
|
|
*
|
|
* to specify localized text for each of these properties.
|
|
*/
|
|
_getLocalizablePropertyValues: function(item, property) {
|
|
// These are localizable properties that a language pack supplied by the
|
|
// Extension may override.
|
|
var values = [];
|
|
var prefName = PREF_EM_EXTENSION_FORMAT.replace(/%UUID%/,
|
|
stripPrefix(item.Value, PREFIX_ITEM_URI)) +
|
|
stripPrefix(property.Value, PREFIX_NS_EM);
|
|
var i = 0;
|
|
do {
|
|
try {
|
|
var value = gPref.getComplexValue(prefName + "." + ++i,
|
|
Components.interfaces.nsIPrefLocalizedString);
|
|
if (value.data)
|
|
values.push(EM_L(value.data));
|
|
}
|
|
catch (e) {
|
|
try {
|
|
var value = gPref.getComplexValue(prefName,
|
|
Components.interfaces.nsIPrefLocalizedString);
|
|
if (value.data)
|
|
values.push(EM_L(value.data));
|
|
}
|
|
catch (e) {
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
while (1);
|
|
return values;
|
|
},
|
|
|
|
/**
|
|
* Get the em:name property (name of the item - template builder calls
|
|
* GetTargets on this one for some reason).
|
|
*/
|
|
_rdfGets_name: function(item, property) {
|
|
return this._getLocalizablePropertyValues(item, property);
|
|
},
|
|
|
|
/**
|
|
* Get the em:contributor property (contributors to the extension)
|
|
*/
|
|
_rdfGets_contributor: function(item, property) {
|
|
return this._getLocalizablePropertyValues(item, property);
|
|
},
|
|
|
|
/**
|
|
* See nsIRDFDataSource.idl
|
|
*/
|
|
GetTargets: function(source, property, truthValue) {
|
|
if (!source)
|
|
return null;
|
|
|
|
var ary = null;
|
|
var getter = "_rdfGets_" + stripPrefix(source.Value, EM_NS);
|
|
if (getter in this)
|
|
ary = this[getter](source, property);
|
|
|
|
return ary ? new ArrayEnumerator(ary)
|
|
: this._inner.GetTargets(source, property, truthValue);
|
|
},
|
|
|
|
Assert: function(source, property, target, truthValue) {
|
|
this._inner.Assert(source, property, target, truthValue);
|
|
},
|
|
|
|
Unassert: function(source, property, target) {
|
|
this._inner.Unassert(source, property, target);
|
|
},
|
|
|
|
Change: function(source, property, oldTarget, newTarget) {
|
|
this._inner.Change(source, property, oldTarget, newTarget);
|
|
},
|
|
|
|
Move: function(oldSource, newSource, property, target) {
|
|
this._inner.Move(oldSource, newSource, property, target);
|
|
},
|
|
|
|
HasAssertion: function(source, property, target, truthValue) {
|
|
if (!source || !property || !target)
|
|
return false;
|
|
return this._inner.HasAssertion(source, property, target, truthValue);
|
|
},
|
|
|
|
_observers: [],
|
|
AddObserver: function(observer) {
|
|
for (var i = 0; i < this._observers.length; ++i) {
|
|
if (this._observers[i] == observer)
|
|
return;
|
|
}
|
|
this._observers.push(observer);
|
|
this._inner.AddObserver(observer);
|
|
},
|
|
|
|
RemoveObserver: function(observer) {
|
|
for (var i = 0; i < this._observers.length; ++i) {
|
|
if (this._observers[i] == observer)
|
|
this._observers.splice(i, 1);
|
|
}
|
|
this._inner.RemoveObserver(observer);
|
|
},
|
|
|
|
ArcLabelsIn: function(node) {
|
|
return this._inner.ArcLabelsIn(node);
|
|
},
|
|
|
|
ArcLabelsOut: function(source) {
|
|
return this._inner.ArcLabelsOut(source);
|
|
},
|
|
|
|
GetAllResources: function() {
|
|
return this._inner.GetAllResources();
|
|
},
|
|
|
|
IsCommandEnabled: function(sources, command, arguments) {
|
|
return this._inner.IsCommandEnabled(sources, command, arguments);
|
|
},
|
|
|
|
DoCommand: function(sources, command, arguments) {
|
|
this._inner.DoCommand(sources, command, arguments);
|
|
},
|
|
|
|
GetAllCmds: function(source) {
|
|
return this._inner.GetAllCmds(source);
|
|
},
|
|
|
|
hasArcIn: function(node, arc) {
|
|
return this._inner.hasArcIn(node, arc);
|
|
},
|
|
|
|
hasArcOut: function(source, arc) {
|
|
return this._inner.hasArcOut(source, arc);
|
|
},
|
|
|
|
beginUpdateBatch: function() {
|
|
return this._inner.beginUpdateBatch();
|
|
},
|
|
|
|
endUpdateBatch: function() {
|
|
return this._inner.endUpdateBatch();
|
|
},
|
|
|
|
/**
|
|
* See nsIRDFRemoteDataSource.idl
|
|
*/
|
|
get loaded() {
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
},
|
|
|
|
Init: function(uri) {
|
|
},
|
|
|
|
Refresh: function(blocking) {
|
|
},
|
|
|
|
Flush: function() {
|
|
if (this._inner instanceof Components.interfaces.nsIRDFRemoteDataSource)
|
|
this._inner.Flush();
|
|
},
|
|
|
|
FlushTo: function(uri) {
|
|
},
|
|
|
|
/**
|
|
* See nsISupports.idl
|
|
*/
|
|
QueryInterface: function(iid) {
|
|
if (!iid.equals(Components.interfaces.nsIRDFDataSource) &&
|
|
!iid.equals(Components.interfaces.nsIRDFRemoteDataSource) &&
|
|
!iid.equals(Components.interfaces.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
function UpdateItem () {
|
|
}
|
|
UpdateItem.prototype = {
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
init: function(id, version, installLocationKey, minAppVersion, maxAppVersion,
|
|
name, downloadURL, xpiHash, iconURL, updateURL, type) {
|
|
this._id = id;
|
|
this._version = version;
|
|
this._installLocationKey = installLocationKey;
|
|
this._minAppVersion = minAppVersion;
|
|
this._maxAppVersion = maxAppVersion;
|
|
this._name = name;
|
|
this._downloadURL = downloadURL;
|
|
this._xpiHash = xpiHash;
|
|
this._iconURL = iconURL;
|
|
this._updateURL = updateURL;
|
|
this._type = type;
|
|
},
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get id() { return this._id; },
|
|
get version() { return this._version; },
|
|
get installLocationKey(){ return this._installLocationKey;},
|
|
get minAppVersion() { return this._minAppVersion; },
|
|
get maxAppVersion() { return this._maxAppVersion; },
|
|
get name() { return this._name; },
|
|
get xpiURL() { return this._downloadURL; },
|
|
get xpiHash() { return this._xpiHash; },
|
|
get iconURL() { return this._iconURL },
|
|
get updateRDF() { return this._updateURL; },
|
|
get type() { return this._type; },
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get objectSource() {
|
|
return { id : this._id,
|
|
version : this._version,
|
|
installLocationKey : this._installLocationKey,
|
|
minAppVersion : this._minAppVersion,
|
|
maxAppVersion : this._maxAppVersion,
|
|
name : this._name,
|
|
xpiURL : this._downloadURL,
|
|
xpiHash : this._xpiHash,
|
|
iconURL : this._iconURL,
|
|
updateRDF : this._updateURL,
|
|
type : this._type
|
|
}.toSource();
|
|
},
|
|
|
|
/**
|
|
* See nsISupports.idl
|
|
*/
|
|
QueryInterface: function(iid) {
|
|
if (!iid.equals(Components.interfaces.nsIUpdateItem) &&
|
|
!iid.equals(Components.interfaces.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
var gModule = {
|
|
registerSelf: function(componentManager, fileSpec, location, type) {
|
|
componentManager = componentManager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
|
|
|
|
for (var key in this._objects) {
|
|
var obj = this._objects[key];
|
|
componentManager.registerFactoryLocation(obj.CID, obj.className, obj.contractID,
|
|
fileSpec, location, type);
|
|
}
|
|
|
|
// Make the Extension Manager a startup observer
|
|
var categoryManager = Components.classes["@mozilla.org/categorymanager;1"]
|
|
.getService(Components.interfaces.nsICategoryManager);
|
|
categoryManager.addCategoryEntry("app-startup", this._objects.manager.className,
|
|
"service," + this._objects.manager.contractID,
|
|
true, true, null);
|
|
},
|
|
|
|
getClassObject: function(componentManager, cid, iid) {
|
|
if (!iid.equals(Components.interfaces.nsIFactory))
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
|
|
for (var key in this._objects) {
|
|
if (cid.equals(this._objects[key].CID))
|
|
return this._objects[key].factory;
|
|
}
|
|
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
},
|
|
|
|
_makeFactory: #1= function(ctor) {
|
|
return {
|
|
createInstance: function (outer, iid) {
|
|
if (outer != null)
|
|
throw Components.results.NS_ERROR_NO_AGGREGATION;
|
|
return (new ctor()).QueryInterface(iid);
|
|
}
|
|
};
|
|
},
|
|
|
|
_objects: {
|
|
manager: { CID : ExtensionManager.prototype.classID,
|
|
contractID : ExtensionManager.prototype.contractID,
|
|
className : ExtensionManager.prototype.classDescription,
|
|
factory : #1#(ExtensionManager)
|
|
},
|
|
item: { CID : Components.ID("{F3294B1C-89F4-46F8-98A0-44E1EAE92518}"),
|
|
contractID : "@mozilla.org/updates/item;1",
|
|
className : "Update Item",
|
|
factory : #1#(UpdateItem)
|
|
}
|
|
},
|
|
|
|
canUnload: function(componentManager) {
|
|
return true;
|
|
}
|
|
};
|
|
|
|
function NSGetModule(compMgr, fileSpec) {
|
|
return gModule;
|
|
}
|
|
|