| /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
| /* |
| //@line 44 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\mozapps\extensions\src\nsExtensionManager.js.in" |
| */ |
| |
| // |
| // TODO: |
| // - better logging |
| // |
| |
| const Cc = Components.classes; |
| const Ci = Components.interfaces; |
| const Cr = Components.results; |
| |
| Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
| |
| const PREF_EM_CHECK_COMPATIBILITY = "extensions.checkCompatibility"; |
| const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity"; |
| const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion"; |
| const PREF_EM_ENABLED_ITEMS = "extensions.enabledItems"; |
| const PREF_UPDATE_COUNT = "extensions.update.count"; |
| const PREF_UPDATE_DEFAULT_URL = "extensions.update.url"; |
| const PREF_EM_NEW_ADDONS_LIST = "extensions.newAddons"; |
| const PREF_EM_IGNOREMTIMECHANGES = "extensions.ignoreMTimeChanges"; |
| const PREF_EM_DISABLEDOBSOLETE = "extensions.disabledObsolete"; |
| const PREF_EM_EXTENSION_FORMAT = "extensions.%UUID%."; |
| const PREF_EM_ITEM_UPDATE_ENABLED = "extensions.%UUID%.update.enabled"; |
| const PREF_EM_UPDATE_ENABLED = "extensions.update.enabled"; |
| const PREF_EM_ITEM_UPDATE_URL = "extensions.%UUID%.update.url"; |
| const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; |
| const PREF_DSS_SWITCHPENDING = "extensions.dss.switchPending"; |
| const PREF_DSS_SKIN_TO_SELECT = "extensions.lastSelectedSkin"; |
| const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin"; |
| const PREF_EM_LOGGING_ENABLED = "extensions.logging.enabled"; |
| const PREF_EM_UPDATE_INTERVAL = "extensions.update.interval"; |
| const PREF_UPDATE_NOTIFYUSER = "extensions.update.notifyUser"; |
| const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; |
| const PREF_SELECTED_LOCALE = "general.useragent.locale"; |
| |
| const DIR_EXTENSIONS = "extensions"; |
| const DIR_CHROME = "chrome"; |
| const DIR_STAGE = "staged-xpis"; |
| const FILE_EXTENSIONS = "extensions.rdf"; |
| const FILE_EXTENSION_MANIFEST = "extensions.ini"; |
| const FILE_EXTENSIONS_STARTUP_CACHE = "extensions.cache"; |
| const FILE_EXTENSIONS_LOG = "extensions.log"; |
| const FILE_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_DEFAULT_THEME_JAR = "classic.jar"; |
| const TOOLKIT_ID = "toolkit@mozilla.org" |
| |
| const KEY_PROFILEDIR = "ProfD"; |
| const KEY_PROFILEDS = "ProfDS"; |
| const KEY_APPDIR = "XCurProcD"; |
| const KEY_TEMPDIR = "TmpD"; |
| |
| const EM_ACTION_REQUESTED_TOPIC = "em-action-requested"; |
| const EM_ITEM_INSTALLED = "item-installed"; |
| const EM_ITEM_UPGRADED = "item-upgraded"; |
| const EM_ITEM_UNINSTALLED = "item-uninstalled"; |
| const EM_ITEM_ENABLED = "item-enabled"; |
| const EM_ITEM_DISABLED = "item-disabled"; |
| const EM_ITEM_CANCEL = "item-cancel-action"; |
| |
| const OP_NONE = ""; |
| const OP_NEEDS_INSTALL = "needs-install"; |
| const OP_NEEDS_UPGRADE = "needs-upgrade"; |
| const OP_NEEDS_UNINSTALL = "needs-uninstall"; |
| const OP_NEEDS_ENABLE = "needs-enable"; |
| const OP_NEEDS_DISABLE = "needs-disable"; |
| |
| const KEY_APP_PROFILE = "app-profile"; |
| const KEY_APP_GLOBAL = "app-global"; |
| const KEY_APP_SYSTEM_LOCAL = "app-system-local"; |
| const KEY_APP_SYSTEM_SHARE = "app-system-share"; |
| const KEY_APP_SYSTEM_USER = "app-system-user"; |
| |
| const CATEGORY_INSTALL_LOCATIONS = "extension-install-locations"; |
| const CATEGORY_UPDATE_PARAMS = "extension-update-params"; |
| |
| const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; |
| const PREFIX_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_EXTENSIONS_PROPERTIES = "chrome://mozapps/locale/extensions/extensions.properties"; |
| const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties"; |
| const URI_DOWNLOADS_PROPERTIES = "chrome://mozapps/locale/downloads/downloads.properties"; |
| const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul"; |
| const URI_EXTENSION_LIST_DIALOG = "chrome://mozapps/content/extensions/list.xul"; |
| |
| const 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 INSTALLERROR_BLOCKLISTED = -6; |
| const INSTALLERROR_INSECURE_UPDATE = -7; |
| const INSTALLERROR_INVALID_MANIFEST = -8; |
| const INSTALLERROR_RESTRICTED = -9; |
| |
| const MODE_RDONLY = 0x01; |
| 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 gBlocklist = null; |
| var gXPCOMABI = null; |
| var gOSTarget = null; |
| var gConsole = null; |
| var gInstallManifestRoot = null; |
| var gVersionChecker = null; |
| var gLoggingEnabled = null; |
| var gCheckCompatibility = true; |
| var gCheckUpdateSecurity = true; |
| var gLocale = "en-US"; |
| var gFirstRun = false; |
| var gAllowFlush = true; |
| var gDSNeedsFlush = false; |
| var gManifestNeedsFlush = false; |
| |
| /** |
| * 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; |
| |
| // shared code for suppressing bad cert dialogs |
| //@line 40 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\mozapps\shared\src\badCertHandler.js" |
| |
| /** |
| * Only allow built-in certs for HTTPS connections. See bug 340198. |
| */ |
| function checkCert(channel) { |
| if (!channel.originalURI.schemeIs("https")) // bypass |
| return; |
| |
| const Ci = Components.interfaces; |
| var cert = |
| channel.securityInfo.QueryInterface(Ci.nsISSLStatusProvider). |
| SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert; |
| |
| var issuer = cert.issuer; |
| while (issuer && !cert.equals(issuer)) { |
| cert = issuer; |
| issuer = cert.issuer; |
| } |
| |
| if (!issuer || issuer.tokenName != "Builtin Object Token") |
| throw "cert issuer is not built-in"; |
| } |
| |
| /** |
| * This class implements nsIBadCertListener. It's job is to prevent "bad cert" |
| * security dialogs from being shown to the user. It is better to simply fail |
| * if the certificate is bad. See bug 304286. |
| */ |
| function BadCertHandler() { |
| } |
| BadCertHandler.prototype = { |
| |
| // nsIChannelEventSink |
| onChannelRedirect: function(oldChannel, newChannel, flags) { |
| // make sure the certificate of the old channel checks out before we follow |
| // a redirect from it. See bug 340198. |
| checkCert(oldChannel); |
| }, |
| |
| // Suppress any certificate errors |
| notifyCertProblem: function(socketInfo, status, targetSite) { |
| return true; |
| }, |
| |
| // Suppress any ssl errors |
| notifySSLError: function(socketInfo, error, targetSite) { |
| return true; |
| }, |
| |
| // nsIInterfaceRequestor |
| getInterface: function(iid) { |
| return this.QueryInterface(iid); |
| }, |
| |
| // nsISupports |
| QueryInterface: function(iid) { |
| if (!iid.equals(Components.interfaces.nsIChannelEventSink) && |
| !iid.equals(Components.interfaces.nsIBadCertListener2) && |
| !iid.equals(Components.interfaces.nsISSLErrorListener) && |
| !iid.equals(Components.interfaces.nsIInterfaceRequestor) && |
| !iid.equals(Components.interfaces.nsISupports)) |
| throw Components.results.NS_ERROR_NO_INTERFACE; |
| return this; |
| } |
| }; |
| //@line 191 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\mozapps\extensions\src\nsExtensionManager.js.in" |
| |
| /** |
| * Creates a Version Checker object. |
| * @returns A handle to the global Version Checker service. |
| */ |
| function getVersionChecker() { |
| if (!gVersionChecker) { |
| gVersionChecker = Cc["@mozilla.org/xpcom/version-comparator;1"]. |
| getService(Ci.nsIVersionComparator); |
| } |
| return gVersionChecker; |
| } |
| |
| var BundleManager = { |
| /** |
| * Creates and returns a String Bundle at the specified URI |
| * @param bundleURI |
| * The URI of the bundle to load |
| * @returns A nsIStringBundle which was retrieved. |
| */ |
| getBundle: function(bundleURI) { |
| var sbs = Cc["@mozilla.org/intl/stringbundle;1"]. |
| getService(Ci.nsIStringBundleService); |
| return sbs.createBundle(bundleURI); |
| }, |
| |
| _appName: "", |
| |
| /** |
| * The Application's display name. |
| */ |
| get appName() { |
| if (!this._appName) { |
| var brandBundle = this.getBundle(URI_BRAND_PROPERTIES) |
| this._appName = brandBundle.GetStringFromName("brandShortName"); |
| } |
| return this._appName; |
| } |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // |
| // Utility Functions |
| // |
| function EM_NS(property) { |
| return PREFIX_NS_EM + property; |
| } |
| |
| function 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); |
| } |
| |
| function EM_D(integer) { |
| return gRDF.GetDateLiteral(integer); |
| } |
| |
| /** |
| * Gets a preference value, handling the case where there is no default. |
| * @param func |
| * The name of the preference function to call, on nsIPrefBranch |
| * @param preference |
| * The name of the preference |
| * @param defaultValue |
| * The default value to return in the event the preference has |
| * no setting |
| * @returns The value of the preference, or undefined if there was no |
| * user or default value. |
| */ |
| function getPref(func, preference, defaultValue) { |
| try { |
| return gPref[func](preference); |
| } |
| catch (e) { |
| } |
| return defaultValue; |
| } |
| |
| /** |
| * Initializes a RDF Container at a URI in a datasource. |
| * @param datasource |
| * The datasource the container is in |
| * @param root |
| * The RDF Resource which is the root of the container. |
| * @returns The nsIRDFContainer, initialized at the root. |
| */ |
| function getContainer(datasource, root) { |
| var ctr = Cc["@mozilla.org/rdf/container;1"]. |
| createInstance(Ci.nsIRDFContainer); |
| ctr.Init(datasource, root); |
| return ctr; |
| } |
| |
| /** |
| * Gets a RDF Resource for item with the given ID |
| * @param id |
| * The GUID of the item to construct a RDF resource to the |
| * active item for |
| * @returns The RDF Resource to the Active item. |
| */ |
| function getResourceForID(id) { |
| return gRDF.GetResource(PREFIX_ITEM_URI + id); |
| } |
| |
| /** |
| * Construct a nsIUpdateItem with the supplied metadata |
| * ... |
| */ |
| function makeItem(id, version, locationKey, minVersion, maxVersion, name, |
| updateURL, updateHash, iconURL, updateRDF, updateKey, type, |
| targetAppID) { |
| var item = new UpdateItem(); |
| item.init(id, version, locationKey, minVersion, maxVersion, name, |
| updateURL, updateHash, iconURL, updateRDF, updateKey, type, |
| targetAppID); |
| return item; |
| } |
| |
| /** |
| * Gets the specified directory at the specified 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 specified 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 specified 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 = Cc["@mozilla.org/file/directory_service;1"]. |
| getService(Ci.nsIProperties); |
| var dir = fileLocator.get(key, Ci.nsILocalFile); |
| for (var i = 0; i < pathArray.length; ++i) { |
| dir.append(pathArray[i]); |
| if (shouldCreate && !dir.exists()) |
| dir.create(Ci.nsILocalFile.DIRECTORY_TYPE, PERMS_DIRECTORY); |
| } |
| dir.followLinks = false; |
| return dir; |
| } |
| |
| /** |
| * Gets the file at the specified 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 = Cc["@mozilla.org/file/local;1"]. |
| createInstance(Ci.nsILocalFile); |
| |
| var m = descriptor.match(/^(abs|rel)\%(.*)$/); |
| if (!m) |
| throw Cr.NS_ERROR_INVALID_ARG; |
| |
| if (m[1] == "rel") { |
| location.setRelativeDescriptor(installLocation.location, m[2]); |
| } |
| else { |
| location.persistentDescriptor = m[2]; |
| } |
| |
| return location; |
| } |
| |
| /** |
| * Determines if a file is an item package - either a XPI or a JAR file. |
| * @param file |
| * The file to check |
| * @returns true if the file is an item package, false otherwise. |
| */ |
| function fileIsItemPackage(file) { |
| var fileURL = getURIFromFile(file); |
| if (fileURL instanceof Ci.nsIURL) |
| var extension = fileURL.fileExtension.toLowerCase(); |
| return extension == "xpi" || extension == "jar"; |
| } |
| |
| /** |
| * 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 = Cc["@mozilla.org/network/safe-file-output-stream;1"]. |
| createInstance(Ci.nsIFileOutputStream); |
| if (modeFlags === undefined) |
| modeFlags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; |
| if (!file.exists()) |
| file.create(Ci.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 Ci.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(Ci.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"); |
| if (gConsole) |
| gConsole.logStringMessage(string); |
| } |
| } |
| |
| /** |
| * Logs a string to the error console and to a permanent log file. |
| * @param string |
| * The string to write out. |
| */ |
| function ERROR(string) { |
| LOG(string); |
| try { |
| var tstamp = new Date(); |
| var logfile = getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS_LOG]); |
| var stream = Cc["@mozilla.org/network/file-output-stream;1"]. |
| createInstance(Ci.nsIFileOutputStream); |
| stream.init(logfile, 0x02 | 0x08 | 0x10, 0666, 0); // write, create, append |
| var writer = Cc["@mozilla.org/intl/converter-output-stream;1"]. |
| createInstance(Ci.nsIConverterOutputStream); |
| writer.init(stream, "UTF-8", 0, 0x0000); |
| string = tstamp.toLocaleFormat("%Y-%m-%d %H:%M:%S - ") + string; |
| writer.writeString(string + "\n"); |
| writer.close(); |
| } |
| catch (e) { } |
| } |
| |
| /** |
| * Randomize the specified file name. Used to force RDF to bypass the cache |
| * when loading certain types of files. |
| * @param fileName |
| * A file name to randomize, e.g. install.rdf |
| * @returns A randomized file name, e.g. install-xyz.rdf |
| */ |
| function getRandomFileName(fileName) { |
| var extensionDelimiter = fileName.lastIndexOf("."); |
| var prefix = fileName.substr(0, extensionDelimiter); |
| var suffix = fileName.substr(extensionDelimiter); |
| |
| var characters = "abcdefghijklmnopqrstuvwxyz0123456789"; |
| var nameString = prefix + "-"; |
| for (var i = 0; i < 3; ++i) { |
| var index = Math.round((Math.random()) * characters.length); |
| nameString += characters.charAt(index); |
| } |
| return nameString + "." + suffix; |
| } |
| |
| /** |
| * Get the RDF URI prefix of a nsIUpdateItem type. This function should be used |
| * ONLY to support Firefox 1.0 Update RDF files! Item URIs in the datasource |
| * are NOT prefixed. |
| * @param type |
| * The nsIUpdateItem type to find a RDF URI prefix for |
| * @returns The RDF URI prefix. |
| */ |
| function getItemPrefix(type) { |
| if (type & Ci.nsIUpdateItem.TYPE_EXTENSION) |
| return PREFIX_EXTENSION; |
| else if (type & Ci.nsIUpdateItem.TYPE_THEME) |
| return PREFIX_THEME; |
| return PREFIX_ITEM_URI; |
| } |
| |
| /** |
| * Trims a prefix from a string. |
| * @param string |
| * The source string |
| * @param prefix |
| * The prefix to remove. |
| * @returns The suffix (string - prefix) |
| */ |
| function stripPrefix(string, prefix) { |
| return string.substr(prefix.length); |
| } |
| |
| /** |
| * Gets a File URL spec for a nsIFile |
| * @param file |
| * The file to get a file URL spec to |
| * @returns The file URL spec to the file |
| */ |
| function getURLSpecFromFile(file) { |
| var ioServ = Cc["@mozilla.org/network/io-service;1"]. |
| getService(Ci.nsIIOService); |
| var fph = ioServ.getProtocolHandler("file") |
| .QueryInterface(Ci.nsIFileProtocolHandler); |
| return fph.getURLSpecFromFile(file); |
| } |
| |
| /** |
| * Constructs a URI to a spec. |
| * @param spec |
| * The spec to construct a URI to |
| * @returns The nsIURI constructed. |
| */ |
| function newURI(spec) { |
| var ioServ = Cc["@mozilla.org/network/io-service;1"]. |
| getService(Ci.nsIIOService); |
| return ioServ.newURI(spec, null, null); |
| } |
| |
| /** |
| * Constructs a File URI to a nsIFile |
| * @param file |
| * The file to construct a File URI to |
| * @returns The file URI to the file |
| */ |
| function getURIFromFile(file) { |
| var ioServ = Cc["@mozilla.org/network/io-service;1"]. |
| getService(Ci.nsIIOService); |
| return ioServ.newFileURI(file); |
| } |
| |
| /** |
| * @returns Whether or not we are currently running in safe mode. |
| */ |
| function inSafeMode() { |
| return gApp.inSafeMode; |
| } |
| |
| /** |
| * Extract the string value from a RDF Literal or Resource |
| * @param literalOrResource |
| * RDF String Literal or Resource |
| * @returns String value of the literal or resource, or undefined if the object |
| * supplied is not a RDF string literal or resource. |
| */ |
| function stringData(literalOrResource) { |
| if (literalOrResource instanceof Ci.nsIRDFLiteral) |
| return literalOrResource.Value; |
| if (literalOrResource instanceof Ci.nsIRDFResource) |
| return literalOrResource.Value; |
| return undefined; |
| } |
| |
| /** |
| * Extract the integer value of a RDF Literal |
| * @param literal |
| * nsIRDFInt literal |
| * @return integer value of the literal |
| */ |
| function intData(literal) { |
| if (literal instanceof Ci.nsIRDFInt) |
| return literal.Value; |
| return undefined; |
| } |
| |
| /** |
| * Gets a property from an install manifest. |
| * @param installManifest |
| * An Install Manifest datasource to read from |
| * @param property |
| * The name of a proprety to read (sans EM_NS) |
| * @returns The literal value of the property, or undefined if the property has |
| * no value. |
| */ |
| function getManifestProperty(installManifest, property) { |
| var target = installManifest.GetTarget(gInstallManifestRoot, |
| gRDF.GetResource(EM_NS(property)), true); |
| var val = stringData(target); |
| return val === undefined ? intData(target) : val; |
| } |
| |
| /** |
| * Given an Install Manifest Datasource, retrieves the type of item the manifest |
| * describes. |
| * @param installManifest |
| * The Install Manifest Datasource. |
| * @return The nsIUpdateItem type of the item described by the manifest |
| * returns TYPE_EXTENSION if attempts to determine the type fail. |
| */ |
| function getAddonTypeFromInstallManifest(installManifest) { |
| var target = installManifest.GetTarget(gInstallManifestRoot, |
| gRDF.GetResource(EM_NS("type")), true); |
| if (target) { |
| var type = stringData(target); |
| return type === undefined ? intData(target) : parseInt(type); |
| } |
| |
| // Firefox 1.0 and earlier did not support addon-type annotation on the |
| // Install Manifest, so we fall back to a theme-only property to |
| // differentiate. |
| if (getManifestProperty(installManifest, "internalName") !== undefined) |
| return Ci.nsIUpdateItem.TYPE_THEME; |
| |
| // If no type is provided, default to "Extension" |
| return Ci.nsIUpdateItem.TYPE_EXTENSION; |
| } |
| |
| /** |
| * Shows a message about an incompatible Extension/Theme. |
| * @param installData |
| * An Install Data object from |getInstallData| |
| */ |
| function showIncompatibleError(installData) { |
| var extensionStrings = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES); |
| var params = [extensionStrings.GetStringFromName("type-" + installData.type)]; |
| var title = extensionStrings.formatStringFromName("incompatibleTitle", |
| params, params.length); |
| params = [installData.name, installData.version, BundleManager.appName, |
| gApp.version]; |
| var message = extensionStrings.formatStringFromName("incompatibleMessage", |
| params, params.length); |
| var ps = Cc["@mozilla.org/embedcomp/prompt-service;1"]. |
| getService(Ci.nsIPromptService); |
| ps.alert(null, title, message); |
| } |
| |
| /** |
| * Shows a message. |
| * @param titleKey |
| * String key of the title string in the Extensions localization file. |
| * @param messageKey |
| * String key of the message string in the Extensions localization file. |
| * @param messageParams |
| * Array of strings to be substituted into |messageKey|. Can be null. |
| */ |
| function showMessage(titleKey, titleParams, messageKey, messageParams) { |
| var extensionStrings = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES); |
| if (titleParams && titleParams.length > 0) { |
| var title = extensionStrings.formatStringFromName(titleKey, titleParams, |
| titleParams.length); |
| } |
| else |
| title = extensionStrings.GetStringFromName(titleKey); |
| |
| if (messageParams && messageParams.length > 0) { |
| var message = extensionStrings.formatStringFromName(messageKey, messageParams, |
| messageParams.length); |
| } |
| else |
| message = extensionStrings.GetStringFromName(messageKey); |
| var ps = Cc["@mozilla.org/embedcomp/prompt-service;1"]. |
| getService(Ci.nsIPromptService); |
| ps.alert(null, title, message); |
| } |
| |
| /** |
| * Shows a dialog for blocklisted items. |
| * @param items |
| * An array of nsIUpdateItems. |
| * @param fromInstall |
| * Whether this is called from an install or from the blocklist |
| * background check. |
| */ |
| function showBlocklistMessage(items, fromInstall) { |
| var win = null; |
| var params = Cc["@mozilla.org/embedcomp/dialogparam;1"]. |
| createInstance(Ci.nsIDialogParamBlock); |
| params.SetInt(0, (fromInstall ? 1 : 0)); |
| params.SetInt(1, items.length); |
| params.SetNumberStrings(items.length * 2); |
| for (var i = 0; i < items.length; ++i) |
| params.SetString(i, items[i].name + " " + items[i].version); |
| |
| // if this was initiated from an install try to find the appropriate manager |
| if (fromInstall) { |
| var wm = Cc["@mozilla.org/appshell/window-mediator;1"]. |
| getService(Ci.nsIWindowMediator); |
| win = wm.getMostRecentWindow("Extension:Manager"); |
| } |
| var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. |
| getService(Ci.nsIWindowWatcher); |
| ww.openWindow(win, URI_EXTENSION_LIST_DIALOG, "", |
| "chrome,centerscreen,modal,dialog,titlebar", params); |
| } |
| |
| /** |
| * Gets a zip reader for the file specified. |
| * @param zipFile |
| * A ZIP archive to open with a nsIZipReader. |
| * @return A nsIZipReader for the file specified. |
| */ |
| function getZipReaderForFile(zipFile) { |
| try { |
| var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. |
| createInstance(Ci.nsIZipReader); |
| zipReader.open(zipFile); |
| } |
| catch (e) { |
| zipReader.close(); |
| throw e; |
| } |
| return zipReader; |
| } |
| |
| /** |
| * 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.extract(fileName, file); |
| zipReader.close(); |
| } |
| catch (e) { |
| if (!suppressErrors) { |
| showMessage("missingFileTitle", [], "missingFileMessage", |
| [BundleManager.appName, fileName]); |
| throw e; |
| } |
| } |
| return file; |
| } |
| |
| /** |
| * Gets an Install Manifest datasource from a file. |
| * @param file |
| * The nsIFile that contains the Install Manifest RDF |
| * @returns The Install Manifest datasource |
| */ |
| function getInstallManifest(file) { |
| var uri = getURIFromFile(file); |
| try { |
| var fis = Cc["@mozilla.org/network/file-input-stream;1"]. |
| createInstance(Ci.nsIFileInputStream); |
| fis.init(file, -1, -1, false); |
| var bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. |
| createInstance(Ci.nsIBufferedInputStream); |
| bis.init(fis, 4096); |
| |
| var rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"]. |
| createInstance(Ci.nsIRDFXMLParser) |
| var ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]. |
| createInstance(Ci.nsIRDFDataSource); |
| var listener = rdfParser.parseAsync(ds, uri); |
| var channel = Cc["@mozilla.org/network/input-stream-channel;1"]. |
| createInstance(Ci.nsIInputStreamChannel); |
| channel.setURI(uri); |
| channel.contentStream = bis; |
| channel.QueryInterface(Ci.nsIChannel); |
| channel.contentType = "text/xml"; |
| |
| listener.onStartRequest(channel, null); |
| try { |
| var pos = 0; |
| var count = bis.available(); |
| while (count > 0) { |
| listener.onDataAvailable(channel, null, bis, pos, count); |
| pos += count; |
| count = bis.available(); |
| } |
| listener.onStopRequest(channel, null, Components.results.NS_OK); |
| bis.close(); |
| fis.close(); |
| |
| var arcs = ds.ArcLabelsOut(gInstallManifestRoot); |
| if (arcs.hasMoreElements()) |
| return ds; |
| } |
| catch (e) { |
| listener.onStopRequest(channel, null, e.result); |
| bis.close(); |
| fis.close(); |
| } |
| } |
| catch (e) { } |
| |
| var url = uri.QueryInterface(Ci.nsIURL); |
| showMessage("malformedTitle", [], "malformedMessage", |
| [BundleManager.appName, url.fileName]); |
| return null; |
| } |
| |
| /** |
| * Selects the closest matching localized resource in the given RDF resource |
| * @param aDataSource The datasource to look in |
| * @param aResource The root resource containing the localized sections |
| * @returns The nsIRDFResource of the best em:localized section or null |
| * if no valid match was found |
| */ |
| function findClosestLocalizedResource(aDataSource, aResource) { |
| var localizedProp = EM_R("localized"); |
| var localeProp = EM_R("locale"); |
| |
| // Holds the best matching localized resource |
| var bestmatch = null; |
| // The number of locale parts it matched with |
| var bestmatchcount = 0; |
| // The number of locale parts in the match |
| var bestpartcount = 0; |
| |
| var locales = [gLocale.toLowerCase()]; |
| /* If the current locale is English then it will find a match if there is |
| a valid match for en-US so no point searching that locale too. */ |
| if (locales[0].substring(0, 3) != "en-") |
| locales.push("en-us"); |
| |
| for each (var locale in locales) { |
| var lparts = locale.split("-"); |
| var localizations = aDataSource.GetTargets(aResource, localizedProp, true); |
| while (localizations.hasMoreElements()) { |
| var localized = localizations.getNext().QueryInterface(Ci.nsIRDFNode); |
| var list = aDataSource.GetTargets(localized, localeProp, true); |
| while (list.hasMoreElements()) { |
| var found = stringData(list.getNext().QueryInterface(Ci.nsIRDFNode)); |
| if (!found) |
| continue; |
| |
| found = found.toLowerCase(); |
| |
| // Exact match is returned immediately |
| if (locale == found) |
| return localized; |
| |
| var fparts = found.split("-"); |
| /* If we have found a possible match and this one isn't any longer |
| then we dont need to check further. */ |
| if (bestmatch && fparts.length < bestmatchcount) |
| continue; |
| |
| // Count the number of parts that match |
| var maxmatchcount = Math.min(fparts.length, lparts.length); |
| var matchcount = 0; |
| while (matchcount < maxmatchcount && |
| fparts[matchcount] == lparts[matchcount]) |
| matchcount++; |
| |
| /* If we matched more than the last best match or matched the same and |
| this locale is less specific than the last best match. */ |
| if (matchcount > bestmatchcount || |
| (matchcount == bestmatchcount && fparts.length < bestpartcount)) { |
| bestmatch = localized; |
| bestmatchcount = matchcount; |
| bestpartcount = fparts.length; |
| } |
| } |
| } |
| // If we found a valid match for this locale return it |
| if (bestmatch) |
| return bestmatch; |
| } |
| return null; |
| } |
| |
| /** |
| * An enumeration of items in a JS array. |
| * @constructor |
| */ |
| function ArrayEnumerator(aItems) { |
| if (aItems) { |
| for (var i = 0; i < aItems.length; ++i) { |
| if (!aItems[i]) |
| aItems.splice(i--, 1); |
| } |
| this._contents = aItems; |
| } else { |
| this._contents = []; |
| } |
| } |
| |
| ArrayEnumerator.prototype = { |
| _index: 0, |
| |
| hasMoreElements: function () { |
| 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) { |
| if (files) { |
| for (var i = 0; i < files.length; ++i) { |
| if (!files[i]) |
| files.splice(i--, 1); |
| } |
| this._contents = files; |
| } else { |
| this._contents = []; |
| } |
| } |
| |
| FileEnumerator.prototype = { |
| _index: 0, |
| |
| /** |
| * Gets the next file in the sequence. |
| */ |
| get nextFile() { |
| if (this._index < this._contents.length) |
| return this._contents[this._index++]; |
| return null; |
| }, |
| |
| /** |
| * Stop enumerating. Nothing to do here. |
| */ |
| close: function() { |
| } |
| }; |
| |
| /** |
| * An object which identifies an Install Location for items, where the location |
| * relationship is each item living in a directory named with its GUID under |
| * the directory used when constructing this object. |
| * |
| * e.g. <location>\{GUID1} |
| * <location>\{GUID2} |
| * <location>\{GUID3} |
| * ... |
| * |
| * @param name |
| * The string identifier of this Install Location. |
| * @param location |
| * The directory that contains the items. |
| * @constructor |
| */ |
| function DirectoryInstallLocation(name, location, restricted, priority, independent) { |
| this._name = name; |
| if (location.exists()) { |
| if (!location.isDirectory()) |
| throw new Error("location must be a directoy!"); |
| } |
| else { |
| try { |
| location.create(Ci.nsILocalFile.DIRECTORY_TYPE, 0775); |
| } |
| catch (e) { |
| ERROR("DirectoryInstallLocation: failed to create location " + |
| " directory = " + location.path + ", exception = " + e + "\n"); |
| } |
| } |
| |
| this._location = location; |
| this._locationToIDMap = {}; |
| this._restricted = restricted; |
| this._priority = priority; |
| this._independent = independent; |
| } |
| DirectoryInstallLocation.prototype = { |
| _name : "", |
| _location : null, |
| _locationToIDMap: null, |
| _restricted : false, |
| _priority : 0, |
| _independent : false, |
| _canAccess : null, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| get name() { |
| return this._name; |
| }, |
| |
| /** |
| * Reads a directory linked to in a file. |
| * @param file |
| * The file containing the directory path |
| * @returns A nsILocalFile object representing the linked directory. |
| */ |
| _readDirectoryFromFile: function(file) { |
| var fis = Cc["@mozilla.org/network/file-input-stream;1"]. |
| createInstance(Ci.nsIFileInputStream); |
| fis.init(file, -1, -1, false); |
| var line = { value: "" }; |
| if (fis instanceof Ci.nsILineInputStream) |
| fis.readLine(line); |
| fis.close(); |
| if (line.value) { |
| var linkedDirectory = Cc["@mozilla.org/file/local;1"]. |
| createInstance(Ci.nsILocalFile); |
| try { |
| linkedDirectory.initWithPath(line.value); |
| } |
| catch (e) { |
| linkedDirectory.setRelativeDescriptor(file.parent, line.value); |
| } |
| |
| return linkedDirectory; |
| } |
| return null; |
| }, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| get itemLocations() { |
| var locations = []; |
| if (!this._location.exists()) |
| return new FileEnumerator(locations); |
| |
| try { |
| var entries = this._location.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); |
| while (true) { |
| var entry = entries.nextFile; |
| if (!entry) |
| break; |
| entry instanceof Ci.nsILocalFile; |
| if (!entry.isDirectory() && gIDTest.test(entry.leafName)) { |
| var linkedDirectory = this._readDirectoryFromFile(entry); |
| if (linkedDirectory && linkedDirectory.exists() && |
| linkedDirectory.isDirectory()) { |
| locations.push(linkedDirectory); |
| this._locationToIDMap[linkedDirectory.persistentDescriptor] = entry.leafName; |
| } |
| } |
| else |
| locations.push(entry); |
| } |
| entries.close(); |
| } |
| catch (e) { |
| } |
| return new FileEnumerator(locations); |
| }, |
| |
| /** |
| * Retrieves the GUID for an item at the specified location. |
| * @param file |
| * The location where an item might live. |
| * @returns The ID for an item that might live at the location specified. |
| * |
| * N.B. This function makes no promises about whether or not this path is |
| * actually maintained by this Install Location. |
| */ |
| getIDForLocation: function(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(Ci.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(Ci.nsILocalFile.DIRECTORY_TYPE, PERMS_DIRECTORY); |
| return itemLocation; |
| }, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| itemIsManagedIndependently: function(id) { |
| if (this._independent) |
| return true; |
| 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(Ci.nsIDirectoryEnumerator); |
| while (entries.hasMoreElements()) { |
| var file = entries.nextFile; |
| if (!(file instanceof Ci.nsILocalFile)) |
| continue; |
| if (file.isDirectory()) |
| removeDirRecursive(file); |
| else if (fileIsItemPackage(file)) { |
| if (stageFile) |
| stageFile.remove(false); |
| stageFile = file; |
| } |
| else |
| file.remove(false); |
| } |
| } |
| catch (e) { |
| } |
| if (entries instanceof Ci.nsIDirectoryEnumerator) |
| entries.close(); |
| return stageFile; |
| }, |
| |
| /** |
| * Removes a file from the stage. This cleans up the stage if there is nothing |
| * else left after the remove operation. |
| * @param file |
| * The file to remove. |
| */ |
| removeFile: function(file) { |
| if (file.exists()) |
| file.remove(false); |
| var parent = file.parent; |
| var entries = parent.directoryEntries; |
| try { |
| // XXXrstrong calling hasMoreElements on a nsIDirectoryEnumerator after |
| // it has been removed will cause a crash on Mac OS X - bug 292823 |
| while (parent && !parent.equals(this.location) && |
| !entries.hasMoreElements()) { |
| parent.remove(false); |
| parent = parent.parent; |
| entries = parent.directoryEntries; |
| } |
| if (entries instanceof Ci.nsIDirectoryEnumerator) |
| entries.close(); |
| } |
| catch (e) { |
| ERROR("DirectoryInstallLocation::removeFile: failed to remove staged " + |
| " directory = " + parent.path + ", exception = " + e + "\n"); |
| } |
| }, |
| |
| QueryInterface: XPCOMUtils.generateQI([Ci.nsIInstallLocation]) |
| }; |
| |
| //@line 1354 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\mozapps\extensions\src\nsExtensionManager.js.in" |
| |
| const nsIWindowsRegKey = Ci.nsIWindowsRegKey; |
| |
| /** |
| * An object that identifies the location of installed items based on entries |
| * in the Windows registry. For each application a subkey is defined that |
| * contains a set of values, where the name of each value is a GUID and the |
| * contents of the value is a filesystem path identifying a directory |
| * containing an installed item. |
| * |
| * @param name |
| * The string identifier of this Install Location. |
| * @param rootKey |
| * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey). |
| * @param restricted |
| * Indicates that the location may be restricted (e.g., this is |
| * usually true of a system level install location). |
| * @param priority |
| * The priority of this install location. |
| * @constructor |
| */ |
| function WinRegInstallLocation(name, rootKey, restricted, priority) { |
| this._name = name; |
| this._rootKey = rootKey; |
| this._restricted = restricted; |
| this._priority = priority; |
| this._IDToDirMap = {}; |
| this._DirToIDMap = {}; |
| |
| // Reading the registry may throw an exception, and that's ok. In error |
| // cases, we just leave ourselves in the empty state. |
| try { |
| var path = this._appKeyPath + "\\Extensions"; |
| var key = Cc["@mozilla.org/windows-registry-key;1"]. |
| createInstance(nsIWindowsRegKey); |
| key.open(this._rootKey, path, nsIWindowsRegKey.ACCESS_READ); |
| this._readAddons(key); |
| } catch (e) { |
| if (key) |
| key.close(); |
| } |
| } |
| WinRegInstallLocation.prototype = { |
| _name : "", |
| _rootKey : null, |
| _restricted : false, |
| _priority : 0, |
| _IDToDirMap : null, // mapping from ID to directory object |
| _DirToIDMap : null, // mapping from directory path to ID |
| |
| /** |
| * Retrieves the path of this Application's data key in the registry. |
| */ |
| get _appKeyPath() { |
| var appVendor = gApp.vendor; |
| var appName = gApp.name; |
| |
| //@line 1416 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\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 = Cc["@mozilla.org/file/local;1"]. |
| createInstance(Ci.nsILocalFile); |
| dir.initWithPath(key.readStringValue(id)); |
| |
| if (dir.exists() && dir.isDirectory()) { |
| this._IDToDirMap[id] = dir; |
| this._DirToIDMap[dir.path] = id; |
| } |
| } |
| }, |
| |
| get name() { |
| return this._name; |
| }, |
| |
| get itemLocations() { |
| var locations = []; |
| for (var id in this._IDToDirMap) { |
| locations.push(this._IDToDirMap[id]); |
| } |
| return new FileEnumerator(locations); |
| }, |
| |
| get location() { |
| return null; |
| }, |
| |
| get restricted() { |
| return this._restricted; |
| }, |
| |
| // you should never be able to write to this location |
| get canAccess() { |
| return false; |
| }, |
| |
| get priority() { |
| return this._priority; |
| }, |
| |
| getItemLocation: function(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: XPCOMUtils.generateQI([Ci.nsIInstallLocation]) |
| }; |
| |
| //@line 1499 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\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; |
| } |
| 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) { |
| var metadataFile = this._installLocation |
| .getItemFile(this._id, FILE_INSTALL_MANIFEST); |
| if (!metadataFile.exists()) |
| return null; |
| this._metadataDS = getInstallManifest(metadataFile); |
| if (!this._metadataDS) { |
| LOG("Installer::install: metadata datasource for extension " + |
| this._id + " at " + 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 == Ci.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) { |
| /** |
| * 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.hasMore()) { |
| var entryName = entries.getNext(); |
| var target = installLocation.getItemFile(extensionID, entryName); |
| if (!target.exists()) { |
| try { |
| target.create(Ci.nsILocalFile.DIRECTORY_TYPE, PERMS_DIRECTORY); |
| } |
| catch (e) { |
| ERROR("extractExtensionsFiles: failed to create target directory for extraction " + |
| " file = " + target.path + ", exception = " + e + "\n"); |
| } |
| } |
| } |
| |
| entries = zipReader.findEntries(null); |
| while (entries.hasMore()) { |
| var entryName = entries.getNext(); |
| target = installLocation.getItemFile(extensionID, entryName); |
| if (target.exists()) |
| continue; |
| |
| try { |
| target.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE); |
| } |
| catch (e) { |
| ERROR("extractExtensionsFiles: failed to create target file for extraction " + |
| " file = " + target.path + ", exception = " + e + "\n"); |
| } |
| zipReader.extract(entryName, target); |
| } |
| zipReader.close(); |
| } |
| |
| /** |
| * 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 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.hasMore()) { |
| var entryName = entries.getNext(); |
| if (entryName.charAt(entryName.length - 1) == "/") |
| continue; |
| target = installLocation.getItemFile(id, entryName); |
| try { |
| target.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE); |
| } |
| catch (e) { |
| ERROR("extractThemeFiles: failed to create target file for extraction " + |
| " file = " + target.path + ", exception = " + e + "\n"); |
| } |
| zipReader.extract(entryName, target); |
| } |
| zipReader.close(); |
| } |
| else { // old theme structure requires only an install.rdf |
| try { |
| var contentsManifestFile = installLocation.getItemFile(id, FILE_CONTENTS_MANIFEST); |
| contentsManifestFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE); |
| zipReader.extract(FILE_CONTENTS_MANIFEST, contentsManifestFile); |
| } |
| catch (e) { |
| zipReader.close(); |
| ERROR("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.leafName); |
| } |
| catch (e) { |
| ERROR("extractThemeFiles: failed to copy theme JAR file to: " + chromeDir.path); |
| throw e; // let the safe-op clean up |
| } |
| |
| if (!installer.metadataDS && installer._type == Ci.nsIUpdateItem.TYPE_THEME) { |
| var themeName = extensionStrings.GetStringFromName("incompatibleThemeName"); |
| if (contentsManifestFile && contentsManifestFile.exists()) { |
| var contentsManifest = gRDF.GetDataSourceBlocking(getURLSpecFromFile(contentsManifestFile)); |
| 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(Ci.nsIRDFResource); |
| themeName = stringData(contentsManifest.GetTarget(elt, nameArc, true)); |
| if (themeName) |
| break; |
| } |
| } |
| catch (e) { |
| themeName = extensionStrings.GetStringFromName("incompatibleThemeName"); |
| } |
| } |
| showIncompatibleError({ name: themeName, version: "", |
| type: Ci.nsIUpdateItem.TYPE_THEME }); |
| 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 installer = this; |
| var callback = extractExtensionFiles; |
| if (this._type == Ci.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 = Cc["@mozilla.org/chrome/chrome-registry;1"]. |
| getService(Ci.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(Ci.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::_finishOperations| |
| ERROR("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(Ci.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(Ci.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(Ci.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(Ci.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 = Cc["@mozilla.org/chrome/chrome-registry;1"]. |
| getService(Ci.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::_finishOperations| |
| ERROR("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) { |
| ERROR("safeInstallOperation: failed to roll back files after an install " + |
| "operation failed. Failed to roll back: " + newFile.path + " to: " + |
| oldFile.path + " ... aborting installation."); |
| throw e; |
| } |
| } |
| } |
| |
| /** |
| * Moves a file to a new folder. |
| * @param file |
| * The file to move |
| * @param destination |
| * The target folder |
| */ |
| function moveFile(file, destination) { |
| try { |
| var oldFile = file.clone(); |
| file.moveTo(destination, file.leafName); |
| movedFiles.push({ oldFile: oldFile, newFile: file }); |
| } |
| catch (e) { |
| ERROR("safeInstallOperation: failed to back up file: " + file.path + " to: " + |
| destination.path + " ... rolling back file moves and aborting " + |
| "installation."); |
| rollbackMove(); |
| throw e; |
| } |
| } |
| |
| /** |
| * Moves a directory to a new location. If any part of the move fails, |
| * files already moved will be rolled back. |
| * @param sourceDir |
| * The directory to move |
| * @param targetDir |
| * The destination directory |
| * @param currentDir |
| * The current directory (a subdirectory of |sourceDir| or |
| * |sourceDir| itself) we are moving files from. |
| */ |
| function moveDirectory(sourceDir, targetDir, currentDir) { |
| var entries = currentDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); |
| while (true) { |
| var entry = entries.nextFile; |
| if (!entry) |
| break; |
| if (entry.isDirectory()) |
| moveDirectory(sourceDir, targetDir, entry); |
| else if (entry instanceof Ci.nsILocalFile) { |
| var rd = entry.getRelativeDescriptor(sourceDir); |
| var destination = targetDir.clone().QueryInterface(Ci.nsILocalFile); |
| destination.setRelativeDescriptor(targetDir, rd); |
| moveFile(entry, destination.parent); |
| } |
| } |
| entries.close(); |
| } |
| |
| /** |
| * Removes the temporary backup directory where we stored files. |
| * @param directory |
| * The backup directory to remove |
| */ |
| function cleanUpTrash(directory) { |
| try { |
| // Us-generated. Safe. |
| if (directory && directory.exists()) |
| removeDirRecursive(directory); |
| } |
| catch (e) { |
| ERROR("safeInstallOperation: failed to clean up the temporary backup of the " + |
| "older version: " + itemLocationTrash.path); |
| // This is a non-fatal error. Annoying, but non-fatal. |
| } |
| } |
| |
| if (!installLocation.itemIsManagedIndependently(itemID)) { |
| var itemLocation = installLocation.getItemLocation(itemID); |
| if (itemLocation.exists()) { |
| var trashDirName = itemID + "-trash"; |
| var itemLocationTrash = itemLocation.parent.clone(); |
| itemLocationTrash.append(trashDirName); |
| if (itemLocationTrash.exists()) { |
| // We can remove recursively here since this is a folder we created, not |
| // one the user specified. If this fails, it'll throw, and the caller |
| // should stop installation. |
| try { |
| removeDirRecursive(itemLocationTrash); |
| } |
| catch (e) { |
| ERROR("safeFileOperation: failed to remove existing trash directory " + |
| itemLocationTrash.path + " ... aborting installation."); |
| throw e; |
| } |
| } |
| |
| // Move the directory that contains the existing version of the item aside, |
| // into {GUID}-trash. This will throw if there's a failure and the install |
| // will abort. |
| moveDirectory(itemLocation, itemLocationTrash, itemLocation); |
| |
| // Clean up the original location, if necessary. Again, this is a path we |
| // generated, so it is safe to recursively delete. |
| try { |
| removeDirRecursive(itemLocation); |
| } |
| catch (e) { |
| ERROR("safeInstallOperation: failed to clean up item location after its contents " + |
| "were properly backed up. Failed to clean up: " + itemLocation.path + |
| " ... rolling back file moves and aborting installation."); |
| rollbackMove(); |
| cleanUpTrash(itemLocationTrash); |
| throw e; |
| } |
| } |
| } |
| else if (installLocation.name == KEY_APP_PROFILE || |
| installLocation.name == KEY_APP_GLOBAL || |
| installLocation.name == KEY_APP_SYSTEM_USER) { |
| // Check for a pointer file and move it aside if it exists |
| var pointerFile = installLocation.location.clone(); |
| pointerFile.append(itemID); |
| if (pointerFile.exists() && !pointerFile.isDirectory()) { |
| var trashFileName = itemID + "-trash"; |
| var itemLocationTrash = installLocation.location.clone(); |
| itemLocationTrash.append(trashFileName); |
| if (itemLocationTrash.exists()) { |
| // We can remove recursively here since this is a folder we created, not |
| // one the user specified. If this fails, it'll throw, and the caller |
| // should stop installation. |
| try { |
| removeDirRecursive(itemLocationTrash); |
| } |
| catch (e) { |
| ERROR("safeFileOperation: failed to remove existing trash directory " + |
| itemLocationTrash.path + " ... aborting installation."); |
| throw e; |
| } |
| } |
| itemLocationTrash.create(Ci.nsILocalFile.DIRECTORY_TYPE, PERMS_DIRECTORY); |
| // Move the pointer file to the trash. |
| moveFile(pointerFile, itemLocationTrash); |
| } |
| } |
| |
| // 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. |
| ERROR("safeInstallOperation: install operation (caller-supplied callback) failed, " + |
| "rolling back file moves and aborting installation."); |
| try { |
| // Us-generated. Safe. |
| removeDirRecursive(itemLocation); |
| } |
| catch (e) { |
| ERROR("safeInstallOperation: failed to remove the folder we failed to install " + |
| "an item into: " + itemLocation.path + " -- There is not much to suggest " + |
| "here... maybe restart and try again?"); |
| cleanUpTrash(itemLocationTrash); |
| throw e; |
| } |
| rollbackMove(); |
| cleanUpTrash(itemLocationTrash); |
| throw e; |
| } |
| |
| // 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 == OP_NONE) |
| this.clearOpsForItem(entry.id); |
| else { |
| if (!(opType in this._ops)) |
| this._ops[opType] = { }; |
| this._ops[opType][entry.id] = entry.locationKey; |
| } |
| }, |
| |
| /** |
| * Removes a Pending Operation from the list |
| * @param opType |
| * The type of Operation being removed |
| * @param id |
| * The GUID of the item to remove the entry for |
| */ |
| clearItem: function(opType, id) { |
| if (opType in this._ops && id in this._ops[opType]) |
| delete this._ops[opType][id]; |
| }, |
| |
| /** |
| * Removes all Pending Operation for an item |
| * @param id |
| * The ID of the item to remove the entries for |
| */ |
| clearOpsForItem: function(id) { |
| for (var opType in this._ops) { |
| if (id in this._ops[opType]) |
| delete this._ops[opType][id]; |
| } |
| }, |
| |
| /** |
| * Remove all Pending Operations of a certain type |
| * @param opType |
| * The type of Operation to remove all entries for |
| */ |
| clearItems: function(opType) { |
| if (opType in this._ops) |
| delete this._ops[opType]; |
| }, |
| |
| /** |
| * Get an array of operations of a certain type |
| * @param opType |
| * The type of Operation to return a list of |
| */ |
| getOperations: function(opType) { |
| if (!(opType in this._ops)) |
| return []; |
| var ops = []; |
| for (var id in this._ops[opType]) |
| ops.push( {id: id, locationKey: this._ops[opType][id] } ); |
| return ops; |
| }, |
| |
| /** |
| * The total number of Pending Operations, for all types. |
| */ |
| get size() { |
| var size = 0; |
| for (var opType in this._ops) { |
| for (var id in this._ops[opType]) |
| ++size; |
| } |
| return size; |
| } |
| }; |
| |
| /** |
| * Manages registered Install Locations |
| */ |
| var InstallLocations = { |
| _locations: { }, |
| |
| /** |
| * A nsISimpleEnumerator of all available Install Locations. |
| */ |
| get enumeration() { |
| var installLocations = []; |
| for (var key in this._locations) |
| installLocations.push(InstallLocations.get(key)); |
| return new ArrayEnumerator(installLocations); |
| }, |
| |
| /** |
| * Gets a named Install Location |
| * @param name |
| * The name of the Install Location to get |
| */ |
| get: function(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(Ci.nsILocalFile); |
| descriptor = getDescriptorFromFile(itemLocation, installLocation); |
| if (itemLocation.exists() && itemLocation.isDirectory()) |
| mtime = Math.floor(itemLocation.lastModifiedTime / 1000); |
| } |
| |
| this._putRaw(installLocation.name, id, descriptor, mtime, op, shouldCreate); |
| }, |
| |
| /** |
| * Private helper function for putting an entry into the Startup Cache |
| * without relying on the presence of its associated nsIInstallLocation |
| * instance. |
| * |
| * @param key |
| * The install location name. |
| * @param id |
| * The ID of the item. |
| * @param descriptor |
| * Value returned from absoluteDescriptor. May be null, in which |
| * case the descriptor field is not updated. |
| * @param mtime |
| * The last modified time of the item. May be null, in which case the |
| * descriptor field is not updated. |
| * @param op |
| * The OP code to store with the entry. |
| * @param shouldCreate |
| * Boolean value indicating whether to create or delete the entry. |
| */ |
| _putRaw: function(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; |
| }, |
| |
| /** |
| * 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. |
| gFirstRun = true; |
| return; |
| } |
| var fis = Cc["@mozilla.org/network/file-input-stream;1"]. |
| createInstance(Ci.nsIFileInputStream); |
| fis.init(itemChangeManifest, -1, -1, false); |
| if (fis instanceof Ci.nsILineInputStream) { |
| var line = { value: "" }; |
| var more = false; |
| do { |
| more = fis.readLine(line); |
| if (line.value) { |
| // The Item-Change manifest is formatted like so: |
| // (pd = descriptor) |
| // location-key\tguid-of-item\tpd-to-extension1\tmtime-of-pd\tpending-op |
| // location-key\tguid-of-item\tpd-to-extension2\tmtime-of-pd\tpending-op |
| // ... |
| // We hash on location-key first, because we don't want to have to |
| // spin up the main extensions datasource on every start to determine |
| // the Install Location for an item. |
| // We hash on guid second, because we want a way to quickly determine |
| // item GUID during a check loop that runs on every startup. |
| var parts = line.value.split("\t"); |
| // Silently drop any entries in unknown install locations |
| if (!InstallLocations.get(parts[0])) |
| continue; |
| var op = parts[4]; |
| this._putRaw(parts[0], parts[1], parts[2], parts[3], op, true); |
| if (op) |
| PendingOperations.addItem(op, { locationKey: parts[0], id: parts[1] }); |
| } |
| } |
| while (more); |
| } |
| fis.close(); |
| }, |
| |
| /** |
| * Writes the Startup Cache to disk |
| */ |
| write: function() { |
| 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 = Cc["@mozilla.org/xre/app-info;1"]. |
| getService(Ci.nsIXULAppInfo).QueryInterface(Ci.nsIXULRuntime); |
| gOSTarget = gApp.OS; |
| try { |
| gXPCOMABI = gApp.XPCOMABI; |
| } catch (ex) { |
| // Provide a default for gXPCOMABI. It won't be compared to an |
| // item's metadata (i.e. install.rdf can't specify e.g. WINNT_unknownABI |
| // as targetPlatform), but it will be displayed in error messages and |
| // transmitted to update URLs. |
| gXPCOMABI = UNKNOWN_XPCOM_ABI; |
| } |
| gPref = Cc["@mozilla.org/preferences-service;1"]. |
| getService(Ci.nsIPrefBranch2); |
| |
| gOS = Cc["@mozilla.org/observer-service;1"]. |
| getService(Ci.nsIObserverService); |
| gOS.addObserver(this, "xpcom-shutdown", false); |
| |
| gConsole = Cc["@mozilla.org/consoleservice;1"]. |
| getService(Ci.nsIConsoleService); |
| |
| gRDF = Cc["@mozilla.org/rdf/rdf-service;1"]. |
| getService(Ci.nsIRDFService); |
| gInstallManifestRoot = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT); |
| |
| // Register Global Install Location |
| var appGlobalExtensions = getDirNoCreate(KEY_APPDIR, [DIR_EXTENSIONS]); |
| var priority = Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_GLOBAL; |
| var globalLocation = new DirectoryInstallLocation(KEY_APP_GLOBAL, |
| appGlobalExtensions, true, |
| priority, false); |
| InstallLocations.put(globalLocation); |
| |
| // Register App-Profile Install Location |
| var appProfileExtensions = getDirNoCreate(KEY_PROFILEDS, [DIR_EXTENSIONS]); |
| var priority = Ci.nsIInstallLocation.PRIORITY_APP_PROFILE; |
| var profileLocation = new DirectoryInstallLocation(KEY_APP_PROFILE, |
| appProfileExtensions, false, |
| priority, false); |
| InstallLocations.put(profileLocation); |
| |
| // Register per-user Install Location |
| try { |
| var appSystemUExtensions = getDirNoCreate("XREUSysExt", [gApp.ID]); |
| } |
| catch(e) { } |
| |
| if (appSystemUExtensions) { |
| var priority = Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_USER; |
| var systemLocation = new DirectoryInstallLocation(KEY_APP_SYSTEM_USER, |
| appSystemUExtensions, false, |
| priority, true); |
| |
| InstallLocations.put(systemLocation); |
| } |
| |
| // Register App-System-Shared Install Location |
| try { |
| var appSystemSExtensions = getDirNoCreate("XRESysSExtPD", [gApp.ID]); |
| } |
| catch (e) { } |
| |
| if (appSystemSExtensions) { |
| var priority = Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_GLOBAL + 10; |
| var systemLocation = new DirectoryInstallLocation(KEY_APP_SYSTEM_SHARE, |
| appSystemSExtensions, true, |
| priority, true); |
| InstallLocations.put(systemLocation); |
| } |
| |
| // Register App-System-Local Install Location |
| try { |
| var appSystemLExtensions = getDirNoCreate("XRESysLExtPD", [gApp.ID]); |
| } |
| catch (e) { } |
| |
| if (appSystemLExtensions) { |
| var priority = Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_GLOBAL + 20; |
| var systemLocation = new DirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL, |
| appSystemLExtensions, true, |
| priority, true); |
| InstallLocations.put(systemLocation); |
| } |
| |
| //@line 2488 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\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, |
| Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_GLOBAL + 10)); |
| |
| // Register HKEY_CURRENT_USER Install Location |
| InstallLocations.put( |
| new WinRegInstallLocation("winreg-app-user", |
| nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, |
| false, |
| Ci.nsIInstallLocation.PRIORITY_APP_SYSTEM_USER + 10)); |
| //@line 2502 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\mozapps\extensions\src\nsExtensionManager.js.in" |
| |
| // Register Additional Install Locations |
| var categoryManager = Cc["@mozilla.org/categorymanager;1"]. |
| getService(Ci.nsICategoryManager); |
| var locations = categoryManager.enumerateCategory(CATEGORY_INSTALL_LOCATIONS); |
| while (locations.hasMoreElements()) { |
| var entry = locations.getNext().QueryInterface(Ci.nsISupportsCString).data; |
| var contractID = categoryManager.getCategoryEntry(CATEGORY_INSTALL_LOCATIONS, entry); |
| var location = Cc[contractID].getService(Ci.nsIInstallLocation); |
| InstallLocations.put(location); |
| } |
| } |
| |
| ExtensionManager.prototype = { |
| /** |
| * See nsIObserver.idl |
| */ |
| observe: function(subject, topic, data) { |
| switch (topic) { |
| case "app-startup": |
| gOS.addObserver(this, "profile-after-change", false); |
| gOS.addObserver(this, "quit-application", 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 "quit-application": |
| gOS.removeObserver(this, "profile-after-change"); |
| gOS.removeObserver(this, "quit-application"); |
| break; |
| case "xpcom-shutdown": |
| this._shutdown(); |
| break; |
| case "nsPref:changed": |
| if (data == PREF_EM_LOGGING_ENABLED) |
| this._loggingToggled(); |
| else if (data == PREF_EM_CHECK_COMPATIBILITY || |
| data == PREF_EM_CHECK_UPDATE_SECURITY) |
| this._updateAppDisabledState(); |
| else if ((data == PREF_MATCH_OS_LOCALE) || (data == PREF_SELECTED_LOCALE)) |
| this._updateLocale(); |
| break; |
| } |
| }, |
| |
| /** |
| * Refresh the logging enabled global from preferences when the user changes |
| * the preference settting. |
| */ |
| _loggingToggled: function() { |
| gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false); |
| }, |
| |
| /** |
| * Retrieves the current locale |
| */ |
| _updateLocale: function() { |
| try { |
| if (gPref.getBoolPref(PREF_MATCH_OS_LOCALE)) { |
| var localeSvc = Cc["@mozilla.org/intl/nslocaleservice;1"]. |
| getService(Ci.nsILocaleService); |
| gLocale = localeSvc.getLocaleComponentForUserAgent(); |
| return; |
| } |
| } |
| catch (ex) { |
| } |
| gLocale = gPref.getCharPref(PREF_SELECTED_LOCALE); |
| }, |
| |
| /** |
| * When a preference is toggled that affects whether an item is usable or not |
| * we must app-enable or app-disable the item based on the new settings. |
| */ |
| _updateAppDisabledState: function() { |
| gCheckCompatibility = getPref("getBoolPref", PREF_EM_CHECK_COMPATIBILITY, true); |
| gCheckUpdateSecurity = getPref("getBoolPref", PREF_EM_CHECK_UPDATE_SECURITY, true); |
| var ds = this.datasource; |
| |
| // Enumerate all items |
| var ctr = getContainer(ds, ds._itemRoot); |
| var elements = ctr.GetElements(); |
| while (elements.hasMoreElements()) { |
| var itemResource = elements.getNext().QueryInterface(Ci.nsIRDFResource); |
| |
| // App disable or enable items as necessary |
| // _appEnableItem and _appDisableItem will do nothing if the item is already |
| // in the right state. |
| id = stripPrefix(itemResource.Value, PREFIX_ITEM_URI); |
| if (this._isUsableItem(id)) |
| this._appEnableItem(id); |
| else |
| this._appDisableItem(id); |
| } |
| }, |
| |
| /** |
| * Initialize the system after a profile has been selected. |
| */ |
| _profileSelected: function() { |
| // 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) { |
| } |
| |
| if ("nsICrashReporter" in Ci && gApp instanceof Ci.nsICrashReporter) { |
| // Annotate the crash report with the list of add-ons |
| try { |
| try { |
| gApp.annotateCrashReport("Add-ons", gPref.getCharPref(PREF_EM_ENABLED_ITEMS)); |
| } catch (e) { } |
| gApp.annotateCrashReport("Theme", gPref.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN)); |
| } |
| catch (ex) { |
| // This will fail in unnofficial builds, ignorable error |
| } |
| } |
| |
| gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false); |
| gCheckCompatibility = getPref("getBoolPref", PREF_EM_CHECK_COMPATIBILITY, true); |
| gCheckUpdateSecurity = getPref("getBoolPref", PREF_EM_CHECK_UPDATE_SECURITY, true); |
| gPref.addObserver("extensions.", this, false); |
| gPref.addObserver(PREF_MATCH_OS_LOCALE, this, false); |
| gPref.addObserver(PREF_SELECTED_LOCALE, this, false); |
| this._updateLocale(); |
| }, |
| |
| /** |
| * Notify user that there are new addons updates |
| */ |
| _showUpdatesWindow: function() { |
| if (!getPref("getBoolPref", PREF_UPDATE_NOTIFYUSER, false)) |
| return; |
| |
| const EMURL = "chrome://mozapps/content/extensions/extensions.xul"; |
| const EMFEATURES = "chrome,centerscreen,extra-chrome,dialog,resizable,modal"; |
| |
| var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. |
| getService(Ci.nsIWindowWatcher); |
| var param = Cc["@mozilla.org/supports-array;1"]. |
| createInstance(Ci.nsISupportsArray); |
| var arg = Cc["@mozilla.org/supports-string;1"]. |
| createInstance(Ci.nsISupportsString); |
| arg.data = "updates-only"; |
| param.AppendElement(arg); |
| ww.openWindow(null, EMURL, null, EMFEATURES, param); |
| }, |
| |
| /** |
| * Clean up on application shutdown to avoid leaks. |
| */ |
| _shutdown: function() { |
| if (!gAllowFlush) { |
| // Something went wrong and there are potentially flushes pending. |
| ERROR("Reached _shutdown and without clearing any pending flushes"); |
| try { |
| gAllowFlush = true; |
| if (gManifestNeedsFlush) { |
| gManifestNeedsFlush = false; |
| this._updateManifests(false); |
| } |
| if (gDSNeedsFlush) { |
| gDSNeedsFlush = false; |
| this.datasource.Flush(); |
| } |
| } |
| catch (e) { |
| ERROR("Error flushing caches: " + e); |
| } |
| } |
| |
| gOS.removeObserver(this, "xpcom-shutdown"); |
| |
| // Release strongly held services. |
| gOS = null; |
| if (this._ptr && gRDF) { |
| gRDF.UnregisterDataSource(this._ptr); |
| this._ptr = null; |
| } |
| gRDF = null; |
| if (gPref) { |
| gPref.removeObserver("extensions.", this); |
| gPref.removeObserver(PREF_MATCH_OS_LOCALE, this); |
| gPref.removeObserver(PREF_SELECTED_LOCALE, this); |
| } |
| gPref = null; |
| gConsole = null; |
| gVersionChecker = null; |
| gInstallManifestRoot = null; |
| gApp = null; |
| }, |
| |
| /** |
| * Check for presence of critical Extension system files. If any is missing, |
| * delete the others and signal that the system needs to rebuild them all |
| * from scratch. |
| * @returns true if any critical file is missing and the system needs to |
| * be rebuilt, false otherwise. |
| */ |
| _ensureDatasetIntegrity: function () { |
| 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; |
| |
| // Block attempts to flush for the entire startup |
| gAllowFlush = false; |
| |
| // Configure any items that are being installed, uninstalled or upgraded |
| // by being added, removed or modified by another process. We must do this |
| // on every startup since there is no way we can tell if this has happened |
| // or not! |
| if (this._checkForFileChanges()) |
| isDirty = true; |
| |
| this._showUpdatesWindow(); |
| |
| if (PendingOperations.size != 0) |
| isDirty = true; |
| |
| var needsRestart = false; |
| // Extension Changes |
| if (isDirty) { |
| needsRestart = this._finishOperations(); |
| |
| if (forceAutoReg) { |
| this._extensionListChanged = true; |
| needsRestart = true; |
| } |
| } |
| |
| // Resume flushing and perform a flush for anything that was deferred |
| try { |
| gAllowFlush = true; |
| if (gManifestNeedsFlush) { |
| gManifestNeedsFlush = false; |
| this._updateManifests(false); |
| } |
| if (gDSNeedsFlush) { |
| gDSNeedsFlush = false; |
| this.datasource.Flush(); |
| } |
| } |
| catch (e) { |
| ERROR("Error flushing caches: " + e); |
| } |
| |
| if (!needsRestart) |
| this._startTimers(); |
| |
| return needsRestart; |
| }, |
| |
| /** |
| * Begins all background update check timers |
| */ |
| _startTimers: function() { |
| // Register a background update check timer |
| var tm = Cc["@mozilla.org/updates/timer-manager;1"]. |
| getService(Ci.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)) |
| return; |
| |
| var items = this.getItemList(Ci.nsIUpdateItem.TYPE_ANY, { }); |
| |
| var updater = new ExtensionItemUpdater(this); |
| updater.checkForUpdates(items, items.length, |
| Ci.nsIExtensionManager.UPDATE_CHECK_NEWVERSION, |
| new BackgroundUpdateCheckListener(this.datasource)); |
| }, |
| |
| /** |
| * 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) { |
| ERROR("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!"); |
| //@line 2866 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\mozapps\extensions\src\nsExtensionManager.js.in" |
| // make sure the file is local on Windows |
| file.normalize(); |
| if (file.path[1] != ':') |
| throw new Error("Can't install global chrome from non-local file "+file.path); |
| //@line 2871 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\mozapps\extensions\src\nsExtensionManager.js.in" |
| 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 == Ci.nsIUpdateItem.TYPE_THEME) |
| installer.upgradeThemeChrome(); |
| else |
| installer.upgradeExtensionChrome(); |
| }, |
| |
| /** |
| * 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"), |
| getManifestProperty(installManifest, "updateKey"), |
| installData.type, |
| targetAppInfo ? targetAppInfo.appID : gApp.ID); |
| } |
| return null; |
| }, |
| |
| /** |
| * Configure an item that was installed or upgraded by another process |
| * so that |_finishOperations| can properly complete processing and |
| * registration. |
| * As this is the only point at which we can reliably know the Install |
| * Location of this item, we use this as an opportunity to: |
| * 1. Check that this item is compatible with this Firefox version. |
| * 2. If it is, configure the item by using the supplied callback. |
| * We do not do any special handling in the case that the item is |
| * not compatible with this version other than to simply not register |
| * it and log that fact - there is no "phone home" check for updates. |
| * It may or may not make sense to do this, but for now we'll just |
| * not register. |
| * @param id |
| * The GUID of the item to validate and configure. |
| * @param location |
| * The Install Location where this item is installed. |
| * @param callback |
| * The callback that configures the item for installation upon |
| * successful validation. |
| */ |
| installItem: function(id, location, callback) { |
| // As this is the only pint at which we reliably know the installation |
| var installRDF = location.getItemFile(id, FILE_INSTALL_MANIFEST); |
| if (installRDF.exists()) { |
| LOG("Item Installed/Upgraded at Install Location: " + location.name + |
| " Item ID: " + id + ", attempting to register..."); |
| var installManifest = getInstallManifest(installRDF); |
| var installData = this._getInstallData(installManifest); |
| if (installData.error == INSTALLERROR_SUCCESS) { |
| LOG("... success, item is compatible"); |
| callback(installManifest, installData.id, location, installData.type); |
| } |
| else if (installData.error == INSTALLERROR_INCOMPATIBLE_VERSION) { |
| LOG("... success, item installed but is not compatible"); |
| callback(installManifest, installData.id, location, installData.type); |
| this._appDisableItem(id); |
| } |
| else if (installData.error == INSTALLERROR_INSECURE_UPDATE) { |
| LOG("... success, item installed but does not provide updates securely"); |
| callback(installManifest, installData.id, location, installData.type); |
| this._appDisableItem(id); |
| } |
| else if (installData.error == INSTALLERROR_BLOCKLISTED) { |
| LOG("... success, item installed but is blocklisted"); |
| callback(installManifest, installData.id, location, installData.type); |
| this._appDisableItem(id); |
| } |
| else { |
| /** |
| * Turns an error code into a message for logging |
| * @param error |
| * an Install Error code |
| * @returns A string message to be logged. |
| */ |
| function translateErrorMessage(error) { |
| switch (error) { |
| case INSTALLERROR_INVALID_GUID: |
| return "Invalid GUID"; |
| case INSTALLERROR_INVALID_VERSION: |
| return "Invalid Version"; |
| case INSTALLERROR_INCOMPATIBLE_PLATFORM: |
| return "Incompatible Platform"; |
| } |
| } |
| LOG("... failure, item is not compatible, error: " + |
| translateErrorMessage(installData.error)); |
| |
| // Add the item to the Startup Cache anyway, so we don't re-detect it |
| // every time the app starts. |
| StartupCache.put(location, id, OP_NONE, true); |
| } |
| } |
| }, |
| |
| /** |
| * Check for changes to items that were made independently of the Extension |
| * Manager, e.g. items were added or removed from a Install Location or items |
| * in an Install Location changed. |
| */ |
| _checkForFileChanges: function() { |
| var em = this; |
| |
| /** |
| * Determines if an item can be used based on whether or not the install |
| * location of the "item" has an equal or higher priority than the install |
| * location where another version may live. |
| * @param id |
| * The GUID of the item being installed. |
| * @param location |
| * The location where an item is to be installed. |
| * @returns true if the item can be installed at that location, false |
| * otherwise. |
| */ |
| function canUse(id, location) { |
| for (var locationKey in StartupCache.entries) { |
| if (locationKey != location.name && |
| id in StartupCache.entries[locationKey]) { |
| if (StartupCache.entries[locationKey][id]) { |
| var oldInstallLocation = InstallLocations.get(locationKey); |
| if (oldInstallLocation.priority <= location.priority) |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Gets a Dialog Param Block loaded with a set of strings to initialize the |
| * XPInstall Confirmation Dialog. |
| * @param strings |
| * An array of strings |
| * @returns A nsIDialogParamBlock loaded with the strings and dialog state. |
| */ |
| function getParamBlock(strings) { |
| var dpb = Cc["@mozilla.org/embedcomp/dialogparam;1"]. |
| createInstance(Ci.nsIDialogParamBlock); |
| // OK and Cancel Buttons |
| dpb.SetInt(0, 2); |
| // Number of Strings |
| dpb.SetInt(1, strings.length); |
| dpb.SetNumberStrings(strings.length); |
| // Add Strings |
| for (var i = 0; i < strings.length; ++i) |
| dpb.SetString(i, strings[i]); |
| |
| var supportsString = Cc["@mozilla.org/supports-string;1"]. |
| createInstance(Ci.nsISupportsString); |
| var bundle = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES); |
| supportsString.data = bundle.GetStringFromName("droppedInWarning"); |
| var objs = Cc["@mozilla.org/array;1"]. |
| createInstance(Ci.nsIMutableArray); |
| objs.appendElement(supportsString, false); |
| dpb.objects = objs; |
| return dpb; |
| } |
| |
| /** |
| * Installs a set of files which were dropped into an install location by |
| * the user, only after user confirmation. |
| * @param droppedInFiles |
| * An array of JS objects with the following properties: |
| * "file" The nsILocalFile where the XPI lives |
| * "location" The Install Location where the XPI was found. |
| * @param xpinstallStrings |
| * An array of strings used to initialize the XPInstall Confirm |
| * dialog. |
| */ |
| function installDroppedInFiles(droppedInFiles, xpinstallStrings) { |
| if (droppedInFiles.length == 0) |
| return; |
| |
| var dpb = getParamBlock(xpinstallStrings); |
| var ifptr = Cc["@mozilla.org/supports-interface-pointer;1"]. |
| createInstance(Ci.nsISupportsInterfacePointer); |
| ifptr.data = dpb; |
| ifptr.dataIID = Ci.nsIDialogParamBlock; |
| var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. |
| getService(Ci.nsIWindowWatcher); |
| ww.openWindow(null, URI_XPINSTALL_CONFIRM_DIALOG, |
| "", "chrome,centerscreen,modal,dialog,titlebar", ifptr); |
| if (!dpb.GetInt(0)) { |
| // User said OK - install items |
| for (var i = 0; i < droppedInFiles.length; ++i) { |
| em.installItemFromFile(droppedInFiles[i].file, |
| droppedInFiles[i].location.name); |
| // We are responsible for cleaning up this file |
| droppedInFiles[i].file.remove(false); |
| } |
| } |
| else { |
| for (i = 0; i < droppedInFiles.length; ++i) { |
| // We are responsible for cleaning up this file |
| droppedInFiles[i].file.remove(false); |
| } |
| } |
| } |
| |
| var isDirty = false; |
| var ignoreMTimeChanges = getPref("getBoolPref", PREF_EM_IGNOREMTIMECHANGES, |
| false); |
| StartupCache.read(); |
| |
| // Array of objects with 'location' and 'id' properties to maybe install. |
| var newItems = []; |
| |
| var droppedInFiles = []; |
| var xpinstallStrings = []; |
| |
| // Enumerate over the install locations from low to high priority. The |
| // enumeration returned is pre-sorted. |
| var installLocations = this.installLocations; |
| while (installLocations.hasMoreElements()) { |
| var location = installLocations.getNext().QueryInterface(Ci.nsIInstallLocation); |
| |
| // Hash the set of items actually held by the Install Location. |
| var actualItems = { }; |
| var entries = location.itemLocations; |
| while (true) { |
| var entry = entries.nextFile; |
| if (!entry) |
| break; |
| |
| // Is this location a valid item? It must be a directory, and contain |
| // an install.rdf manifest: |
| if (entry.isDirectory()) { |
| var installRDF = entry.clone(); |
| installRDF.append(FILE_INSTALL_MANIFEST); |
| |
| var id = location.getIDForLocation(entry); |
| if (!id || (!installRDF.exists() && |
| !location.itemIsManagedIndependently(id))) |
| continue; |
| |
| actualItems[id] = entry; |
| } |
| else { |
| // Check to see if this file is a XPI/JAR dropped into this dir |
| // by the user, installing it if necessary. We do this here rather |
| // than separately in |_finishOperations| because I don't want to |
| // walk these lists multiple times on every startup. |
| var item = this._getItemForDroppedFile(entry, location); |
| if (item) { |
| droppedInFiles.push({ file: entry, location: location }); |
| var prettyName = ""; |
| try { |
| var zipReader = getZipReaderForFile(entry); |
| 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) { } |
| if (zipReader) |
| zipReader.close(); |
| xpinstallStrings = xpinstallStrings.concat([item.name, |
| getURLSpecFromFile(entry), |
| item.iconURL, |
| prettyName]); |
| isDirty = true; |
| } |
| } |
| } |
| |
| if (location.name in StartupCache.entries) { |
| // Look for items that have been uninstalled by removing their directory. |
| for (var id in StartupCache.entries[location.name]) { |
| if (!StartupCache.entries[location.name] || |
| !StartupCache.entries[location.name][id]) |
| continue; |
| |
| // Force _finishOperations to run if we have enabled or disabled items. |
| // XXXdarin this should be unnecessary now that we check |
| // PendingOperations.size in start() |
| if (StartupCache.entries[location.name][id].op == OP_NEEDS_ENABLE || |
| StartupCache.entries[location.name][id].op == OP_NEEDS_DISABLE) |
| isDirty = true; |
| |
| if (!(id in actualItems) && |
| StartupCache.entries[location.name][id].op != OP_NEEDS_UNINSTALL && |
| StartupCache.entries[location.name][id].op != OP_NEEDS_INSTALL && |
| StartupCache.entries[location.name][id].op != OP_NEEDS_UPGRADE) { |
| // We have an entry for this id in the Extensions database, for this |
| // install location, but it no longer exists in the Install Location. |
| // We can infer from this that the item has been removed, so uninstall |
| // it properly. |
| if (canUse(id, location)) { |
| LOG("Item Uninstalled via file removal from: " + StartupCache.entries[location.name][id].descriptor + |
| " Item ID: " + id + " Location Key: " + location.name + ", uninstalling item."); |
| |
| // Load the Extensions Datasource and force this item into the visible |
| // items list if it is not already. This allows us to handle the case |
| // where there is an entry for an item in the Startup Cache but not |
| // in the extensions.rdf file - in that case the item will not be in |
| // the visible list and calls to |getInstallLocation| will mysteriously |
| // fail. |
| this.datasource.updateVisibleList(id, location.name, false); |
| this.uninstallItem(id); |
| isDirty = true; |
| } |
| } |
| else if (!ignoreMTimeChanges) { |
| // Look for items whose mtime has changed, and as such we can assume |
| // they have been "upgraded". |
| var lf = { path: StartupCache.entries[location.name][id].descriptor }; |
| try { |
| lf = getFileFromDescriptor(StartupCache.entries[location.name][id].descriptor, location); |
| } |
| catch (e) { } |
| |
| if (lf.exists && lf.exists()) { |
| var actualMTime = Math.floor(lf.lastModifiedTime / 1000); |
| if (actualMTime != StartupCache.entries[location.name][id].mtime) { |
| LOG("Item Location path changed: " + lf.path + " Item ID: " + |
| id + " Location Key: " + location.name + ", attempting to upgrade item..."); |
| if (canUse(id, location)) { |
| this.installItem(id, location, |
| function(installManifest, id, location, type) { |
| em._upgradeItem(installManifest, id, location, |
| type); |
| }); |
| isDirty = true; |
| } |
| } |
| } |
| else { |
| isDirty = true; |
| LOG("Install Location returned a missing or malformed item path! " + |
| "Item Path: " + lf.path + ", Location Key: " + location.name + |
| " Item ID: " + id); |
| if (canUse(id, location)) { |
| // Load the Extensions Datasource and force this item into the visible |
| // items list if it is not already. This allows us to handle the case |
| // where there is an entry for an item in the Startup Cache but not |
| // in the extensions.rdf file - in that case the item will not be in |
| // the visible list and calls to |getInstallLocation| will mysteriously |
| // fail. |
| this.datasource.updateVisibleList(id, location.name, false); |
| this.uninstallItem(id); |
| } |
| } |
| } |
| } |
| } |
| |
| // Look for items that have been installed by appearing in the location. |
| for (var id in actualItems) { |
| if (!(location.name in StartupCache.entries) || |
| !(id in StartupCache.entries[location.name]) || |
| !StartupCache.entries[location.name][id]) { |
| // Remember that we've seen this item |
| StartupCache.put(location, id, OP_NONE, true); |
| // Push it on the stack of items to maybe install later |
| newItems.push({location: location, id: id}); |
| } |
| } |
| } |
| |
| // Process any newly discovered items. We do this here instead of in the |
| // previous loop so that we can be sure that we have a fully populated |
| // StartupCache. |
| for (var i = 0; i < newItems.length; ++i) { |
| var id = newItems[i].id; |
| var location = newItems[i].location; |
| if (canUse(id, location)) { |
| LOG("Item Installed via directory addition to Install Location: " + |
| location.name + " Item ID: " + id + ", attempting to register..."); |
| this.installItem(id, location, |
| function(installManifest, id, location, type) { |
| em._configureForthcomingItem(installManifest, id, location, |
| type); |
| }); |
| // Disable add-ons on install when the InstallDisabled file exists. |
| // This is so Talkback will be disabled on a subset of installs. |
| var installDisabled = location.getItemFile(id, "InstallDisabled"); |
| if (installDisabled.exists()) |
| em.disableItem(id); |
| isDirty = true; |
| } |
| } |
| |
| // Ask the user if they want to install the dropped items, for security |
| // purposes. |
| installDroppedInFiles(droppedInFiles, xpinstallStrings); |
| |
| return isDirty; |
| }, |
| |
| /** |
| * 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(Ci.nsIUpdateItem.TYPE_EXTENSION + |
| Ci.nsIUpdateItem.TYPE_LOCALE); |
| 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, |
| Ci.nsIUpdateItem.TYPE_EXTENSION); |
| installer.upgradeExtensionChrome(); |
| } |
| } |
| else { |
| ds.removeItemMetadata(e.id); |
| ds.removeItemFromContainer(e.id); |
| } |
| |
| checkForNewChrome = true; |
| } |
| } |
| |
| var themes = this._getActiveItems(Ci.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(Ci.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 = Cc["@mozilla.org/chrome/chrome-registry;1"]. |
| getService(Ci.nsIToolkitChromeRegistry); |
| cr.processContentsManifest(contentsURI, manifestURI, contentsURI, false, true); |
| } |
| entries.close(); |
| } |
| catch (e) { |
| ERROR("_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; |
| var upgrades = []; |
| var newAddons = []; |
| var addons = getPref("getCharPref", PREF_EM_NEW_ADDONS_LIST, ""); |
| if (addons != "") |
| newAddons = addons.split(","); |
| do { |
| // Enable and disable during startup so items that are changed in the |
| // ui can be reset to a no-op. |
| // Look for extensions that need to be enabled. |
| var items = PendingOperations.getOperations(OP_NEEDS_ENABLE); |
| for (var i = items.length - 1; i >= 0; --i) { |
| var id = items[i].id; |
| var installLocation = this.getInstallLocation(id); |
| StartupCache.put(installLocation, id, OP_NONE, true); |
| PendingOperations.clearItem(OP_NEEDS_ENABLE, id); |
| needsRestart = true; |
| } |
| PendingOperations.clearItems(OP_NEEDS_ENABLE); |
| |
| // Look for extensions that need to be disabled. |
| items = PendingOperations.getOperations(OP_NEEDS_DISABLE); |
| for (i = items.length - 1; i >= 0; --i) { |
| id = items[i].id; |
| installLocation = this.getInstallLocation(id); |
| StartupCache.put(installLocation, id, OP_NONE, true); |
| PendingOperations.clearItem(OP_NEEDS_DISABLE, id); |
| needsRestart = true; |
| } |
| PendingOperations.clearItems(OP_NEEDS_DISABLE); |
| |
| // Look for extensions that need to be upgraded. The process here is to |
| // uninstall the old version of the extension first, then install the |
| // new version in its place. |
| items = PendingOperations.getOperations(OP_NEEDS_UPGRADE); |
| for (i = items.length - 1; i >= 0; --i) { |
| id = items[i].id; |
| var newLocation = InstallLocations.get(items[i].locationKey); |
| // check if there is updated app compatibility info |
| var newTargetAppInfo = ds.getUpdatedTargetAppInfo(id); |
| if (newTargetAppInfo) |
| updatedTargetAppInfos.push(newTargetAppInfo); |
| this._finalizeUpgrade(id, newLocation); |
| upgrades.push(id); |
| } |
| PendingOperations.clearItems(OP_NEEDS_UPGRADE); |
| |
| // Install items |
| items = PendingOperations.getOperations(OP_NEEDS_INSTALL); |
| for (i = items.length - 1; i >= 0; --i) { |
| needsRestart = true; |
| id = items[i].id; |
| // check if there is updated app compatibility info |
| newTargetAppInfo = ds.getUpdatedTargetAppInfo(id); |
| if (newTargetAppInfo) |
| updatedTargetAppInfos.push(newTargetAppInfo); |
| this._finalizeInstall(id, null); |
| if (upgrades.indexOf(id) < 0 && newAddons.indexOf(id) < 0) |
| newAddons.push(id); |
| } |
| PendingOperations.clearItems(OP_NEEDS_INSTALL); |
| |
| // Look for extensions that need to be removed. This MUST be done after |
| // the install operations since extensions to be installed may have to be |
| // uninstalled if there are errors during the installation process! |
| items = PendingOperations.getOperations(OP_NEEDS_UNINSTALL); |
| for (i = items.length - 1; i >= 0; --i) { |
| id = items[i].id; |
| this._finalizeUninstall(id); |
| this._checkForUncoveredItem(id); |
| needsRestart = true; |
| var pos = newAddons.indexOf(id); |
| if (pos >= 0) |
| newAddons.splice(pos, 1); |
| } |
| PendingOperations.clearItems(OP_NEEDS_UNINSTALL); |
| |
| // When there have been operations and all operations have completed. |
| if (PendingOperations.size == 0) { |
| // If there is updated app compatibility info update the datasource. |
| for (i = 0; i < updatedTargetAppInfos.length; ++i) |
| ds.setTargetApplicationInfo(updatedTargetAppInfos[i].id, |
| updatedTargetAppInfos[i].targetAppID, |
| updatedTargetAppInfos[i].minVersion, |
| updatedTargetAppInfos[i].maxVersion, |
| null); |
| |
| // Enumerate all items |
| var ctr = getContainer(ds, ds._itemRoot); |
| var elements = ctr.GetElements(); |
| while (elements.hasMoreElements()) { |
| var itemResource = elements.getNext().QueryInterface(Ci.nsIRDFResource); |
| |
| // Ensure appDisabled is in the correct state. |
| id = stripPrefix(itemResource.Value, PREFIX_ITEM_URI); |
| if (this._isUsableItem(id)) |
| ds.setItemProperty(id, EM_R("appDisabled"), null); |
| else |
| ds.setItemProperty(id, EM_R("appDisabled"), EM_L("true")); |
| |
| // userDisabled is set based on its value being OP_NEEDS_ENABLE or |
| // OP_NEEDS_DISABLE. This allows us to have an item to be enabled |
| // by the app and disabled by the user during a single restart. |
| var value = stringData(ds.GetTarget(itemResource, EM_R("userDisabled"), true)); |
| if (value == OP_NEEDS_ENABLE) |
| ds.setItemProperty(id, EM_R("userDisabled"), null); |
| else if (value == OP_NEEDS_DISABLE) |
| ds.setItemProperty(id, EM_R("userDisabled"), EM_L("true")); |
| } |
| } |
| } |
| while (PendingOperations.size > 0); |
| |
| // Upgrade contents.rdf files to the new chrome.manifest format for |
| // existing Extensions and Themes |
| if (this._upgradeChrome()) { |
| var cr = Cc["@mozilla.org/chrome/chrome-registry;1"]. |
| getService(Ci.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); |
| |
| // Remember the list of add-ons that were installed this time around |
| // unless this was a new profile. |
| if (!gFirstRun && newAddons.length > 0) |
| gPref.setCharPref(PREF_EM_NEW_ADDONS_LIST, newAddons.join(",")); |
| } |
| catch (e) { |
| ERROR("ExtensionManager:_finishOperations - failure, catching exception - lineno: " + |
| e.lineNumber + " - file: " + e.fileName + " - " + e); |
| } |
| return needsRestart; |
| }, |
| |
| /** |
| * Checks to see if there are items that are incompatible with this version |
| * of the application, disables them to prevent incompatibility problems and |
| * invokes the Update Wizard to look for newer versions. |
| * @returns true if there were incompatible items installed and disabled, and |
| * the application must now be restarted to reinitialize XPCOM, |
| * false otherwise. |
| */ |
| checkForMismatches: function() { |
| // Check to see if the version of the application that is being started |
| // now is the same one that was started last time. |
| var currAppVersion = gApp.version; |
| var lastAppVersion = getPref("getCharPref", PREF_EM_LAST_APP_VERSION, ""); |
| if (currAppVersion == lastAppVersion) |
| return false; |
| // With a new profile lastAppVersion doesn't exist yet. |
| if (!lastAppVersion) { |
| gPref.setCharPref(PREF_EM_LAST_APP_VERSION, currAppVersion); |
| return false; |
| } |
| |
| // Block attempts to flush for the entire startup |
| gAllowFlush = false; |
| |
| // 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. |
| 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; |
| // During app upgrade cleanup invalid entries in the extensions datasource. |
| ds.beginUpdateBatch(); |
| var allResources = ds.GetAllResources(); |
| while (allResources.hasMoreElements()) { |
| var res = allResources.getNext().QueryInterface(Ci.nsIRDFResource); |
| if (ds.GetTarget(res, EM_R("downloadURL"), true) || |
| (!ds.GetTarget(res, EM_R("installLocation"), true) && |
| stringData(ds.GetTarget(res, EM_R("appDisabled"), true)) == "true")) |
| ds.removeDownload(res.Value); |
| } |
| ds.endUpdateBatch(); |
| |
| var badItems = []; |
| var allAppManaged = true; |
| var ctr = getContainer(ds, ds._itemRoot); |
| var elements = ctr.GetElements(); |
| while (elements.hasMoreElements()) { |
| var itemResource = elements.getNext().QueryInterface(Ci.nsIRDFResource); |
| var id = stripPrefix(itemResource.Value, PREFIX_ITEM_URI); |
| var location = this.getInstallLocation(id); |
| if (!location) { |
| // Item was in an unknown install location |
| badItems.push(id); |
| continue; |
| } |
| |
| if (ds.getItemProperty(id, "appManaged") == "true") { |
| // Force an update of the metadata for appManaged extensions since the |
| // last modified time is not updated for directories on FAT / FAT32 |
| // filesystems when software update applies a new version of the app. |
| if (location.name == KEY_APP_GLOBAL) { |
| var installRDF = location.getItemFile(id, FILE_INSTALL_MANIFEST); |
| if (installRDF.exists()) { |
| var metadataDS = getInstallManifest(installRDF); |
| ds.addItemMetadata(id, metadataDS, location); |
| ds.updateProperty(id, "compatible"); |
| } |
| } |
| } |
| else if (allAppManaged) |
| allAppManaged = false; |
| |
| var properties = { |
| availableUpdateURL: null, |
| availableUpdateVersion: null |
| }; |
| |
| if (ds.getItemProperty(id, "providesUpdatesSecurely") == "false") { |
| /* It's possible the previous version did not understand updateKeys so |
| * check if we can import one for this addon from it's manifest. */ |
| installRDF = location.getItemFile(id, FILE_INSTALL_MANIFEST); |
| if (installRDF.exists()) { |
| metadataDS = getInstallManifest(installRDF); |
| var literal = metadataDS.GetTarget(gInstallManifestRoot, EM_R("updateKey"), true); |
| if (literal && literal instanceof Ci.nsIRDFLiteral) |
| ds.setItemProperty(id, EM_R("updateKey"), literal); |
| } |
| } |
| |
| // appDisabled is determined by an item being compatible, using secure |
| // updates, satisfying its dependencies, and not being blocklisted |
| if (this._isUsableItem(id)) { |
| if (ds.getItemProperty(id, "appDisabled")) |
| properties.appDisabled = null; |
| } |
| else if (!ds.getItemProperty(id, "appDisabled")) { |
| properties.appDisabled = EM_L("true"); |
| } |
| |
| ds.setItemProperties(id, properties); |
| } |
| |
| // Must clean up outside of the loop. Modifying the container while |
| // iterating its contents is bad. |
| for (var i = 0; i < badItems.length; i++) { |
| id = badItems[i]; |
| LOG("Item " + id + " was installed in an unknown location, removing."); |
| var disabled = ds.getItemProperty(id, "userDisabled") == "true"; |
| // Clean up the datasource |
| ds.removeCorruptItem(id); |
| // Check for any unhidden items. |
| var entries = StartupCache.findEntries(id); |
| if (entries.length > 0) { |
| var newLocation = InstallLocations.get(entries[0].location); |
| for (var j = 1; j < entries.length; j++) { |
| location = InstallLocations.get(entries[j].location); |
| if (newLocation.priority < location.priority) |
| newLocation = location; |
| } |
| LOG("Activating item " + id + " in " + newLocation.name); |
| var em = this; |
| this.installItem(id, newLocation, |
| function(installManifest, id, location, type) { |
| em._configureForthcomingItem(installManifest, id, location, |
| type); |
| }); |
| if (disabled) |
| em.disableItem(id); |
| } |
| } |
| |
| // Update the manifests to reflect the items that were disabled / enabled. |
| this._updateManifests(true); |
| |
| // 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); |
| |
| // Prevent extension update dialog from showing |
| gPref.setBoolPref(PREF_UPDATE_NOTIFYUSER, false); |
| |
| // Re-enable flushing and flush anything that was deferred |
| try { |
| gAllowFlush = true; |
| if (gManifestNeedsFlush) { |
| gManifestNeedsFlush = false; |
| this._updateManifests(false); |
| } |
| if (gDSNeedsFlush) { |
| gDSNeedsFlush = false; |
| this.datasource.Flush(); |
| } |
| } |
| catch (e) { |
| ERROR("Error flushing caches: " + e); |
| } |
| |
| return true; |
| }, |
| |
| /** |
| * Shows the "Compatibility Updates" UI |
| */ |
| _showMismatchWindow: function(items) { |
| var wm = Cc["@mozilla.org/appshell/window-mediator;1"]. |
| getService(Ci.nsIWindowMediator); |
| var wizard = wm.getMostRecentWindow("Update:Wizard"); |
| if (wizard) |
| wizard.focus(); |
| else { |
| var features = "chrome,centerscreen,dialog,titlebar,modal"; |
| // This *must* be modal so as not to break startup! This code is invoked before |
| // the main event loop is initiated (via checkForMismatches). |
| var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. |
| getService(Ci.nsIWindowWatcher); |
| ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, 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() { |
| var extensionsDS = getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS]); |
| var dsExists = extensionsDS.exists(); |
| // Toolkiit 1.7 profiles (Firefox 1.0, Thunderbird 1.0, etc.) have a default |
| // theme directory in the profile's extensions directory that will be |
| // disabled due to having a maxVersion that is incompatible with the |
| // toolkit 1.8 release of the app. |
| var profileDefaultTheme = getDirNoCreate(KEY_PROFILEDS, [DIR_EXTENSIONS, |
| stripPrefix(RDFURI_DEFAULT_THEME, PREFIX_ITEM_URI)]); |
| if (profileDefaultTheme && profileDefaultTheme.exists()) { |
| removeDirRecursive(profileDefaultTheme); |
| // Sunbird 0.3a1 didn't move the default theme into the app's extensions |
| // directory and we can't install it while uninstalling the one in the |
| // profile directory. If we have a toolkit 1.8 extensions datasource and |
| // a profile default theme deleting the toolkit 1.8 extensions datasource |
| // will fix this problem when the datasource is re-created. |
| if (dsExists) |
| extensionsDS.remove(false); |
| } |
| |
| // return early if the toolkit 1.7 extensions datasource file doesn't exist. |
| var oldExtensionsFile = getFile(KEY_PROFILEDIR, [DIR_EXTENSIONS, "Extensions.rdf"]); |
| if (!oldExtensionsFile.exists()) |
| return; |
| |
| // Sunbird 0.2 used a different GUID for the default theme |
| profileDefaultTheme = getDirNoCreate(KEY_PROFILEDS, [DIR_EXTENSIONS, |
| "{8af2d0a7-e394-4de2-ae55-2dae532a7a9b}"]); |
| if (profileDefaultTheme && profileDefaultTheme.exists()) |
| removeDirRecursive(profileDefaultTheme); |
| |
| // Firefox 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); |
| |
| // return early to avoid migrating data twice if we already have a |
| // toolkit 1.8 extension datasource. |
| if (dsExists) |
| return; |
| |
| // 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(Ci.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; |
| if (jarFile.isDirectory()) |
| continue; |
| 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) { |
| } |
| } |
| zipReader.close(); |
| } |
| catch (e) { |
| ERROR("ExtensionManager:_upgradeFromV10 - failed to extract theme files\r\n" + |
| "Exception: " + e); |
| } |
| } |
| } |
| |
| // 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 = gApp.version; |
| var currAppID = gApp.ID; |
| for (var i = 0; i < itemsToCheck.length; ++i) { |
| var item = ds.getItemForID(itemsToCheck[i]); |
| var oldPrefix = (item.type == Ci.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 Ci.nsIRDFResource) { |
| try { |
| var foundAppID = stringData(oldExtensionsDS.GetTarget(targetApp, EM_R("id"), true)); |
| // Different target application? (Note: v1.0 didn't support toolkit app ID) |
| if (foundAppID != currAppID) |
| 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.setTargetApplicationInfo(item.id, foundAppID, updatedMinVersion, |
| updatedMaxVersion, null); |
| |
| 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) { |
| // During startup we block flushing until the startup operations are all |
| // complete to reduce file accesses that can trigger bug 431065 |
| if (gAllowFlush) { |
| // Write the Startup Cache (All Items, visible or not) |
| StartupCache.write(); |
| // Write the Extensions Locations Manifest (Visible, enabled items) |
| this._updateExtensionsManifest(); |
| } |
| else { |
| gManifestNeedsFlush = true; |
| } |
| |
| // Notify nsAppRunner to update the compatibility manifest on next startup |
| this._extensionListChanged = needsRestart; |
| }, |
| |
| /** |
| * Get a list of items that are currently "active" (turned on) of a specific |
| * type |
| * @param type |
| * The nsIUpdateItem type to return a list of items of |
| * @returns An array of active items of the specified type. |
| */ |
| _getActiveItems: function(type) { |
| var allItems = this.getItemList(type, { }); |
| var activeItems = []; |
| var ds = this.datasource; |
| for (var i = 0; i < allItems.length; ++i) { |
| var item = allItems[i]; |
| |
| var installLocation = this.getInstallLocation(item.id); |
| // An entry with an invalid install location is not active. |
| if (!installLocation) |
| continue; |
| // An item entry is valid only if it is not disabled, not about to |
| // be disabled, and not about to be uninstalled. |
| if (installLocation.name in StartupCache.entries && |
| item.id in StartupCache.entries[installLocation.name] && |
| StartupCache.entries[installLocation.name][item.id]) { |
| var op = StartupCache.entries[installLocation.name][item.id].op; |
| if (op == OP_NEEDS_INSTALL || op == OP_NEEDS_UPGRADE || |
| op == OP_NEEDS_UNINSTALL || op == OP_NEEDS_DISABLE) |
| continue; |
| } |
| // Suppress items that have been disabled by the user or the app. |
| if (ds.getItemProperty(item.id, "isDisabled") != "true") |
| activeItems.push({ id: item.id, version: item.version, |
| location: installLocation }); |
| } |
| |
| return activeItems; |
| }, |
| |
| /** |
| * Write the Extensions List |
| */ |
| _updateExtensionsManifest: function() { |
| // When an operation is performed that requires a component re-registration |
| // (extension enabled/disabled, installed, uninstalled), we must write the |
| // set of paths where extensions live so that the startup system can determine |
| // where additional components, preferences, chrome manifests etc live. |
| // |
| // To do this we obtain a list of active extensions and themes and write |
| // these to the extensions.ini file in the profile directory. |
| var validExtensions = this._getActiveItems(Ci.nsIUpdateItem.TYPE_ANY - |
| Ci.nsIUpdateItem.TYPE_THEME); |
| var validThemes = this._getActiveItems(Ci.nsIUpdateItem.TYPE_THEME); |
| |
| var extensionsLocationsFile = getFile(KEY_PROFILEDIR, [FILE_EXTENSION_MANIFEST]); |
| var fos = openSafeFileOutputStream(extensionsLocationsFile); |
| |
| var enabledItems = []; |
| var extensionSectionHeader = "[ExtensionDirs]\r\n"; |
| fos.write(extensionSectionHeader, extensionSectionHeader.length); |
| for (var i = 0; i < validExtensions.length; ++i) { |
| var e = validExtensions[i]; |
| var itemLocation = e.location.getItemLocation(e.id).QueryInterface(Ci.nsILocalFile); |
| var descriptor = getAbsoluteDescriptor(itemLocation); |
| var line = "Extension" + i + "=" + descriptor + "\r\n"; |
| fos.write(line, line.length); |
| enabledItems.push(e.id + ":" + e.version); |
| } |
| |
| var themeSectionHeader = "[ThemeDirs]\r\n"; |
| fos.write(themeSectionHeader, themeSectionHeader.length); |
| for (i = 0; i < validThemes.length; ++i) { |
| var e = validThemes[i]; |
| var itemLocation = e.location.getItemLocation(e.id).QueryInterface(Ci.nsILocalFile); |
| var descriptor = getAbsoluteDescriptor(itemLocation); |
| var line = "Extension" + i + "=" + descriptor + "\r\n"; |
| fos.write(line, line.length); |
| enabledItems.push(e.id + ":" + e.version); |
| } |
| |
| closeSafeFileOutputStream(fos); |
| |
| // Cache the enabled list for annotating the crash report subsequently |
| gPref.setCharPref(PREF_EM_ENABLED_ITEMS, enabledItems.join(",")); |
| }, |
| |
| /** |
| * Say whether or not the Extension List has changed (and thus whether or not |
| * the system will have to restart the next time it is started). |
| * @param val |
| * true if the Extension List has changed, false otherwise. |
| * @returns |val| |
| */ |
| set _extensionListChanged(val) { |
| // When an extension has an operation perform on it (e.g. install, upgrade, |
| // disable, etc.) we are responsible for 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(Ci.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. |
| * INSTALLERROR_INSECURE_UPDATE |
| * error, item has no secure method of providing updates |
| * INSTALLERROR_BLOCKLISTED |
| * error, item is blocklisted |
| */ |
| _getInstallData: function(installManifest) { |
| var installData = { id : "", |
| version : "", |
| name : "", |
| type : 0, |
| error : INSTALLERROR_SUCCESS, |
| targetApps : [], |
| updateURL : "", |
| updateKey : "", |
| currentApp : null }; |
| |
| // Fetch properties from the Install Manifest |
| installData.id = getManifestProperty(installManifest, "id"); |
| installData.version = getManifestProperty(installManifest, "version"); |
| installData.name = getManifestProperty(installManifest, "name"); |
| installData.type = getAddonTypeFromInstallManifest(installManifest); |
| installData.updateURL= getManifestProperty(installManifest, "updateURL"); |
| installData.updateKey= getManifestProperty(installManifest, "updateKey"); |
| |
| /** |
| * Reads a property off a Target Application resource |
| * @param resource |
| * The RDF Resource for a Target Application |
| * @param property |
| * The property (less EM_NS) to read |
| * @returns The string literal value of the property. |
| */ |
| function readTAProperty(resource, property) { |
| return stringData(installManifest.GetTarget(resource, EM_R(property), true)); |
| } |
| |
| var targetApps = installManifest.GetTargets(gInstallManifestRoot, |
| EM_R("targetApplication"), |
| true); |
| while (targetApps.hasMoreElements()) { |
| var targetApp = targetApps.getNext(); |
| if (targetApp instanceof Ci.nsIRDFResource) { |
| try { |
| var data = { id : readTAProperty(targetApp, "id"), |
| minVersion: readTAProperty(targetApp, "minVersion"), |
| maxVersion: readTAProperty(targetApp, "maxVersion") }; |
| installData.targetApps.push(data); |
| if ((data.id == gApp.ID) || |
| (data.id == TOOLKIT_ID) && !installData.currentApp) |
| installData.currentApp = data; |
| } |
| catch (e) { |
| continue; |
| } |
| } |
| } |
| |
| // If the item specifies one or more target platforms, make sure our OS/ABI |
| // combination is in the list - otherwise, refuse to install the item. |
| var targetPlatforms = null; |
| try { |
| targetPlatforms = installManifest.GetTargets(gInstallManifestRoot, |
| EM_R("targetPlatform"), |
| true); |
| } catch(e) { |
| // No targetPlatform nodes, continue. |
| } |
| if (targetPlatforms != null && targetPlatforms.hasMoreElements()) { |
| var foundMatchingOS = false; |
| var foundMatchingOSAndABI = false; |
| var requireABICompatibility = false; |
| while (targetPlatforms.hasMoreElements()) { |
| var targetPlatform = stringData(targetPlatforms.getNext()); |
| var os = targetPlatform.split("_")[0]; |
| var index = targetPlatform.indexOf("_"); |
| var abi = index != -1 ? targetPlatform.substr(index + 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 that the add-on provides a secure update method. |
| if (gCheckUpdateSecurity && |
| installData.updateURL && |
| installData.updateURL.substring(0, 6) != "https:" && |
| !installData.updateKey) { |
| installData.error = INSTALLERROR_INSECURE_UPDATE; |
| return installData; |
| } |
| |
| // Check that the target application range allows compatibility with the app |
| if (gCheckCompatibility && |
| !this.datasource.isCompatible(installManifest, gInstallManifestRoot, undefined)) { |
| installData.error = INSTALLERROR_INCOMPATIBLE_VERSION; |
| return installData; |
| } |
| |
| // Check if the item is blocklisted. |
| if (!gBlocklist) |
| gBlocklist = Cc["@mozilla.org/extensions/blocklist;1"]. |
| getService(Ci.nsIBlocklistService); |
| if (gBlocklist.isAddonBlocklisted(installData.id, installData.version, |
| null, null)) |
| installData.error = INSTALLERROR_BLOCKLISTED; |
| |
| 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); |
| |
| // If there are no compatibility checks running and no downloads in |
| // progress then the install operations are complete. |
| if (this._compatibilityCheckCount == 0 && this._transactions.length == 0) { |
| for (var i = 0; i < this._installListeners.length; ++i) |
| this._installListeners[i].onInstallsCompleted(); |
| } |
| }, |
| |
| /** |
| * 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 && targetApp.id != TOOLKIT_ID) { |
| /* XXXben uncomment when this works! |
| var settingsThingy = Cc[]. |
| getService(Ci.nsIXULRunnerSettingsThingy); |
| try { |
| var appPrefix = "SOFTWARE\\Mozilla\\XULRunner\\Applications\\"; |
| var branch = settingsThingy.getBranch(appPrefix + targetApp.id); |
| var path = branch.getProperty("ExtensionsLocation"); |
| var destination = Cc["@mozilla.org/file/local;1"]. |
| createInstance(Ci.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(Ci.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.hasMore()) { |
| var entryName = entries.getNext(); |
| var target = getFile(KEY_TEMPDIR, [entryName]); |
| try { |
| target.createUnique(Ci.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(entryName, 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; |
| } |
| |
| 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 = { |
| _xpi: null, |
| _installManifest: 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 item |
| * An nsIUpdateItem representing the item being installed. |
| * @param installManifest |
| * The Install Manifest datasource for the item. |
| * @param xpiFile |
| * The staged source XPI file that contains the item. Cleaned |
| * up by this process. |
| * @param installRDF |
| * The install.rdf file that was extracted from the xpi. |
| */ |
| checkForUpdates: function(item, installManifest, xpiFile) { |
| this._xpi = xpiFile; |
| this._installManifest = installManifest; |
| |
| for (var i = 0; i < em._installListeners.length; ++i) |
| em._installListeners[i].onCompatibilityCheckStarted(item); |
| em._compatibilityCheckCount++; |
| em.update([item], 1, Ci.nsIExtensionManager.UPDATE_CHECK_COMPATIBILITY, this); |
| }, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| onUpdateStarted: function() { |
| LOG("Phone Home Listener: Update Started"); |
| }, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| onUpdateEnded: function() { |
| LOG("Phone Home Listener: Update Ended"); |
| }, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| onAddonUpdateStarted: function(addon) { |
| if (!addon) |
| throw Cr.NS_ERROR_INVALID_ARG; |
| |
| LOG("Phone Home Listener: Update For " + addon.id + " started"); |
| em.datasource.addIncompatibleUpdateItem(addon.name, this._xpi.path, |
| addon.type, addon.version); |
| }, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| onAddonUpdateEnded: function(addon, status) { |
| if (!addon) |
| throw Cr.NS_ERROR_INVALID_ARG; |
| |
| LOG("Phone Home Listener: Update For " + addon.id + " ended, status = " + status); |
| em.datasource.removeDownload(this._xpi.path); |
| LOG("Version Check Phone Home Completed"); |
| |
| for (var i = 0; i < em._installListeners.length; ++i) |
| em._installListeners[i].onCompatibilityCheckEnded(addon, status); |
| |
| // Only compatibility updates (e.g. STATUS_VERSIONINFO) are currently |
| // supported |
| if (status == Ci.nsIAddonUpdateCheckListener.STATUS_VERSIONINFO) { |
| em.datasource.setTargetApplicationInfo(addon.id, |
| addon.targetAppID, |
| addon.minAppVersion, |
| addon.maxAppVersion, |
| this._installManifest); |
| |
| // Try and install again, but use the updated compatibility DB. |
| // This will send out an apropriate onInstallEnded notification for us. |
| 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.setTargetApplicationInfo(addon.id, |
| addon.targetAppID, |
| addon.minAppVersion, |
| addon.maxAppVersion, |
| null); |
| } |
| else { // needs a restart |
| // Add updatedMinVersion and updatedMaxVersion so it can be used |
| // to update the datasource during the installation or upgrade. |
| em.datasource.setUpdatedTargetAppInfo(addon.id, |
| addon.targetAppID, |
| addon.minAppVersion, |
| addon.maxAppVersion, |
| null); |
| } |
| } |
| else { |
| em.datasource.removeDownload(this._xpi.path); |
| showIncompatibleError(installData); |
| LOG("Add-on " + addon.id + " is incompatible with " + |
| BundleManager.appName + " " + gApp.version + ", Toolkit " + |
| gApp.platformVersion + ". Remote compatibility check did not " + |
| "resolve this."); |
| |
| for (var i = 0; i < em._installListeners.length; ++i) |
| em._installListeners[i].onInstallEnded(addon, INSTALLERROR_INCOMPATIBLE_VERSION); |
| |
| // We are responsible for cleaning up this file! |
| InstallLocations.get(aInstallLocationKey).removeFile(this._xpi); |
| } |
| |
| em._compatibilityCheckCount--; |
| // If there are no more compatibility checks running and no downloads in |
| // progress then the install operations are complete. |
| if (em._compatibilityCheckCount == 0 && em._transactions.length == 0) { |
| for (var i = 0; i < em._installListeners.length; ++i) |
| em._installListeners[i].onInstallsCompleted(); |
| } |
| }, |
| |
| QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonUpdateCheckListener]) |
| } |
| |
| 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. As this is the first point in the |
| // install process we must notify observers here. |
| |
| var addon = makeItem(getURIFromFile(aXPIFile).spec, "", |
| aInstallLocationKey, "", "", "", |
| getURIFromFile(aXPIFile).spec, |
| "", "", "", "", 0, gApp.id); |
| for (var i = 0; i < this._installListeners.length; ++i) |
| this._installListeners[i].onInstallStarted(addon); |
| |
| shouldPhoneHomeIfNecessary = true; |
| var installManifest = null; |
| var installManifestFile = extractRDFFileToTempDir(aXPIFile, |
| FILE_INSTALL_MANIFEST, |
| true); |
| if (installManifestFile.exists()) { |
| installManifest = getInstallManifest(installManifestFile); |
| installManifestFile.remove(false); |
| } |
| if (!installManifest) { |
| LOG("The Install Manifest supplied by this item is not well-formed. " + |
| "Installation will not proceed."); |
| for (var i = 0; i < this._installListeners.length; ++i) |
| this._installListeners[i].onInstallEnded(addon, INSTALLERROR_INVALID_MANIFEST); |
| return; |
| } |
| } |
| else |
| installManifest = aInstallManifest; |
| |
| var installData = this._getInstallData(installManifest); |
| // Recreate the add-on item with the full detail from the install manifest |
| addon = makeItem(installData.id, installData.version, |
| aInstallLocationKey, |
| installData.currentApp ? installData.currentApp.minVersion : "", |
| installData.currentApp ? installData.currentApp.maxVersion : "", |
| installData.name, |
| getURIFromFile(aXPIFile).spec, |
| "", /* XPI Update Hash */ |
| "", /* Icon URL */ |
| installData.updateURL || "", |
| installData.updateKey || "", |
| installData.type, |
| installData.currentApp ? installData.currentApp.id : ""); |
| |
| 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) |
| return; |
| var stagedFile = installLocation.stageFile(aXPIFile, installData.id); |
| (new IncompatibleObserver(this)).checkForUpdates(addon, installManifest, |
| stagedFile); |
| // 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); |
| LOG("Add-on " + installData.id + " is incompatible with " + |
| BundleManager.appName + " " + gApp.version + ", Toolkit " + |
| gApp.platformVersion + ". Remote compatibility check was not performed."); |
| } |
| break; |
| case INSTALLERROR_SUCCESS: |
| // Installation of multiple extensions / themes contained within a single xpi. |
| if (installData.type == Ci.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. |
| for (var i = 0; i < this._installListeners.length; ++i) |
| this._installListeners[i].onInstallEnded(addon, INSTALLERROR_RESTRICTED); |
| 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, installLocation); |
| this._finalizeInstall(installData.id, stagedFile); |
| } |
| } |
| } |
| else { |
| this._configureForthcomingItem(installManifest, installData.id, |
| installLocation, installData.type); |
| if (!restartRequired) { |
| this._finalizeInstall(installData.id, stagedFile); |
| if (installData.type == Ci.nsIUpdateItem.TYPE_THEME) { |
| var internalName = this.datasource.getItemProperty(installData.id, "internalName"); |
| if (gPref.getBoolPref(PREF_EM_DSS_ENABLED)) { |
| gPref.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, internalName); |
| } |
| else { |
| gPref.setBoolPref(PREF_DSS_SWITCHPENDING, true); |
| gPref.setCharPref(PREF_DSS_SKIN_TO_SELECT, internalName); |
| } |
| } |
| } |
| } |
| 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; |
| case INSTALLERROR_BLOCKLISTED: |
| LOG("Blocklisted Item: Item: \"" + installData.id + "\" version " + |
| installData.version + " was not installed."); |
| showBlocklistMessage([installData], true); |
| break; |
| case INSTALLERROR_INSECURE_UPDATE: |
| LOG("No secure updates: Item: \"" + installData.id + "\" version " + |
| installData.version + " was not installed."); |
| var bundle = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES); |
| showMessage("incompatibleTitle", |
| [bundle.GetStringFromName("type-" + installData.type)], |
| "insecureUpdateMessage", [installData.name]); |
| 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); |
| |
| // The install of this item is complete, notify observers |
| for (var i = 0; i < this._installListeners.length; ++i) |
| this._installListeners[i].onInstallEnded(addon, installData.error); |
| }, |
| |
| /** |
| * 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 Ci.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 ((type & Ci.nsIUpdateItem.TYPE_ADDON) > 0); |
| }, |
| |
| /** |
| * 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 name = null; |
| var localized = findClosestLocalizedResource(installManifest, gInstallManifestRoot); |
| if (localized) |
| name = installManifest.GetTarget(localized, EM_R("name"), true); |
| else |
| name = EM_L(getManifestProperty(installManifest, "name")); |
| |
| var props = { name : name, |
| version : EM_L(getManifestProperty(installManifest, "version")), |
| newVersion : EM_L(getManifestProperty(installManifest, "version")), |
| installLocation : EM_L(installLocation.name), |
| type : EM_I(type), |
| availableUpdateURL : null, |
| availableUpdateHash : null, |
| availableUpdateVersion: null, |
| availableUpdateInfo : null }; |
| ds.setItemProperties(id, props); |
| 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), |
| newVersion : EM_L(getManifestProperty(installManifest, "version")), |
| availableUpdateURL : null, |
| availableUpdateHash : null, |
| availableUpdateVersion : null, |
| availableUpdateInfo : null }; |
| ds.setItemProperties(id, props); |
| 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); |
| 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. |
| * @param installLocation |
| * The nsIInstallLocation of the item |
| */ |
| _finalizeUpgrade: function(id, installLocation) { |
| // Retrieve the item properties *BEFORE* we clean the resource! |
| var ds = this.datasource; |
| |
| 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) { |
| ERROR("_finalizeUninstall: failed to remove directory for item: " + id + |
| " at Install Location: " + installLocation.name + ", rolling back uninstall"); |
| var manifest = installLocation.getItemFile(id, "FILE_INSTALL_MANIFEST"); |
| // If there is no manifest then either the rollback failed, or there was |
| // no manifest in the first place. Either way this item is now invalid |
| // and we shouldn't try to re-install it. |
| if (manifest.exists()) { |
| // 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 || |
| installLocation.name == KEY_APP_SYSTEM_USER) { |
| // 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 ds = this.datasource; |
| ds.updateDownloadState(PREFIX_ITEM_URI + id, null); |
| if (!ds.isDownloadItem(id)) { |
| var opType = ds.getItemProperty(id, "opType"); |
| var installLocation = this.getInstallLocation(id); |
| // Removes any staged xpis for this item. |
| if (opType == OP_NEEDS_UPGRADE || opType == OP_NEEDS_INSTALL) { |
| var stageFile = installLocation.getStageFile(id); |
| if (stageFile) |
| installLocation.removeFile(stageFile); |
| } |
| // Addons with an opType of OP_NEEDS_INSTALL only have a staged xpi file |
| // and are removed immediately since the uninstall can't be canceled. |
| if (opType == OP_NEEDS_INSTALL) { |
| ds.removeItemMetadata(id); |
| ds.removeItemFromContainer(id); |
| ds.updateVisibleList(id, null, true); |
| StartupCache.clearEntry(installLocation, id); |
| this._updateManifests(false); |
| } |
| else { |
| if (opType == OP_NEEDS_UPGRADE) |
| ds.setItemProperty(id, "newVersion", null); |
| this._setOp(id, OP_NEEDS_UNINSTALL); |
| var type = ds.getItemProperty(id, "type"); |
| 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); |
| }, |
| |
| /* See nsIExtensionManager.idl */ |
| cancelInstallItem: function(id) { |
| var ds = this.datasource; |
| var opType = ds.getItemProperty(id, "opType"); |
| if (opType != OP_NEEDS_UPGRADE && opType != OP_NEEDS_INSTALL) |
| return; |
| |
| ds.updateDownloadState(PREFIX_ITEM_URI + id, null); |
| var installLocation = this.getInstallLocation(id); |
| // Removes any staged xpis for this item. |
| var stageFile = installLocation.getStageFile(id); |
| if (stageFile) |
| installLocation.removeFile(stageFile); |
| // Addons with an opType of OP_NEEDS_INSTALL only have a staged xpi file |
| // and just need to be removed completely from the ds. |
| if (opType == OP_NEEDS_INSTALL) { |
| ds.removeItemMetadata(id); |
| ds.removeItemFromContainer(id); |
| ds.updateVisibleList(id, null, true); |
| StartupCache.clearEntry(installLocation, id); |
| this._updateManifests(false); |
| this._notifyAction(id, EM_ITEM_CANCEL); |
| } |
| else { |
| // Clear upgrade information and reset any request to enable/disable. |
| ds.setItemProperty(id, EM_R("newVersion"), null); |
| var appDisabled = ds.getItemProperty(id, "appDisabled"); |
| var userDisabled = ds.getItemProperty(id, "userDisabled"); |
| if (appDisabled == "true" || appDisabled == OP_NONE && userDisabled == OP_NONE) { |
| this._setOp(id, OP_NONE); |
| this._notifyAction(id, EM_ITEM_CANCEL); |
| } |
| else if (appDisabled == OP_NEEDS_DISABLE || userDisabled == OP_NEEDS_DISABLE) { |
| this._setOp(id, OP_NEEDS_DISABLE); |
| this._notifyAction(id, EM_ITEM_DISABLED); |
| } |
| else if (appDisabled == OP_NEEDS_ENABLE || userDisabled == OP_NEEDS_ENABLE) { |
| this._setOp(id, OP_NEEDS_ENABLE); |
| this._notifyAction(id, EM_ITEM_ENABLED); |
| } |
| else { |
| this._setOp(id, OP_NONE); |
| this._notifyAction(id, EM_ITEM_CANCEL); |
| } |
| } |
| }, |
| |
| /** |
| * Cancels a pending uninstall of an item |
| * @param id |
| * The ID of the item. |
| */ |
| cancelUninstallItem: function(id) { |
| var ds = this.datasource; |
| var appDisabled = ds.getItemProperty(id, "appDisabled"); |
| var userDisabled = ds.getItemProperty(id, "userDisabled"); |
| if (appDisabled == "true" || appDisabled == OP_NONE && userDisabled == OP_NONE) { |
| this._setOp(id, OP_NONE); |
| this._notifyAction(id, EM_ITEM_CANCEL); |
| } |
| else if (appDisabled == OP_NEEDS_DISABLE || userDisabled == OP_NEEDS_DISABLE) { |
| this._setOp(id, OP_NEEDS_DISABLE); |
| this._notifyAction(id, EM_ITEM_DISABLED); |
| } |
| else if (appDisabled == OP_NEEDS_ENABLE || userDisabled == OP_NEEDS_ENABLE) { |
| 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 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; |
| if (op == OP_NEEDS_INSTALL || op == OP_NEEDS_UPGRADE) |
| ds.updateDownloadState(PREFIX_ITEM_URI + id, "success"); |
| |
| ds.updateProperty(id, "opType"); |
| ds.updateProperty(id, "updateable"); |
| ds.updateProperty(id, "satisfiesDependencies"); |
| var restartRequired = this.installRequiresRestart(id, ds.getItemProperty(id, "type")) |
| this._updateDependentItemsForID(id); |
| this._updateManifests(restartRequired); |
| }, |
| |
| /** |
| * Note on appDisabled and userDisabled property arcs. |
| * The appDisabled and userDisabled RDF property arcs are used to store |
| * the pending operation for app disabling and user disabling for an item as |
| * well as the user and app disabled status after the pending operation has |
| * been completed upon restart. When the appDisabled value changes the value |
| * of userDisabled is reset to prevent the state of widgets and status |
| * messages from being in an incorrect state. |
| */ |
| |
| /** |
| * Enables an item for the application (e.g. the item satisfies all |
| * requirements like app compatibility for it to be enabled). The appDisabled |
| * property arc will be removed if the item will be app disabled on next |
| * restart to cancel the app disabled operation for the item otherwise the |
| * property value will be set to OP_NEEDS_ENABLE. The item's pending |
| * operations are then evaluated in order to set the operation to perform |
| * and notify the observers if the operation has been changed. |
| * See "Note on appDisabled and userDisabled property arcs" above. |
| * @param id |
| * The ID of the item to be enabled by the application. |
| */ |
| _appEnableItem: function(id) { |
| var ds = this.datasource; |
| var appDisabled = ds.getItemProperty(id, "appDisabled"); |
| if (appDisabled == OP_NONE || appDisabled == OP_NEEDS_ENABLE) |
| return; |
| |
| var opType = ds.getItemProperty(id, "opType"); |
| var userDisabled = ds.getItemProperty(id, "userDisabled"); |
| // reset user disabled if it has a pending operation to prevent the ui |
| // state from getting confused as to an item's current state. |
| if (userDisabled == OP_NEEDS_DISABLE) |
| ds.setItemProperty(id, EM_R("userDisabled"), null); |
| else if (userDisabled == OP_NEEDS_ENABLE) |
| ds.setItemProperty(id, EM_R("userDisabled"), EM_L("true")); |
| |
| if (appDisabled == OP_NEEDS_DISABLE) |
| ds.setItemProperty(id, EM_R("appDisabled"), null); |
| else if (appDisabled == "true") |
| ds.setItemProperty(id, EM_R("appDisabled"), EM_L(OP_NEEDS_ENABLE)); |
| |
| // Don't set a new operation when there is a pending uninstall operation. |
| if (opType == OP_NEEDS_UNINSTALL) { |
| this._updateDependentItemsForID(id); |
| return; |
| } |
| |
| var operation, action; |
| // if this item is already enabled or user disabled don't set a pending |
| // operation - instead immediately enable it and reset the operation type |
| // if needed. |
| if (appDisabled == OP_NEEDS_DISABLE || appDisabled == OP_NONE || |
| userDisabled == "true") { |
| if (opType != OP_NONE) { |
| operation = OP_NONE; |
| action = EM_ITEM_CANCEL; |
| } |
| } |
| else { |
| if (opType != OP_NEEDS_ENABLE) { |
| operation = OP_NEEDS_ENABLE; |
| action = EM_ITEM_ENABLED; |
| } |
| } |
| |
| if (action) { |
| this._setOp(id, operation); |
| this._notifyAction(id, action); |
| } |
| else { |
| ds.updateProperty(id, "satisfiesDependencies"); |
| this._updateDependentItemsForID(id); |
| } |
| }, |
| |
| /** |
| * Disables an item for the application (e.g. the item doesn't satisfy all |
| * requirements like app compatibility for it to be enabled). The appDisabled |
| * property arc will be set to true if the item will be app enabled on next |
| * restart to cancel the app enabled operation for the item otherwise the |
| * property value will be set to OP_NEEDS_DISABLE. The item's pending |
| * operations are then evaluated in order to set the operation to perform |
| * and notify the observers if the operation has been changed. |
| * See "Note on appDisabled and userDisabled property arcs" above. |
| * @param id |
| * The ID of the item to be disabled by the application. |
| */ |
| _appDisableItem: function(id) { |
| var ds = this.datasource; |
| var appDisabled = ds.getItemProperty(id, "appDisabled"); |
| if (appDisabled == "true" || appDisabled == OP_NEEDS_DISABLE) |
| return; |
| |
| var opType = ds.getItemProperty(id, "opType"); |
| var userDisabled = ds.getItemProperty(id, "userDisabled"); |
| |
| // reset user disabled if it has a pending operation to prevent the ui |
| // state from getting confused as to an item's current state. |
| if (userDisabled == OP_NEEDS_DISABLE) |
| ds.setItemProperty(id, EM_R("userDisabled"), null); |
| else if (userDisabled == OP_NEEDS_ENABLE) |
| ds.setItemProperty(id, EM_R("userDisabled"), EM_L("true")); |
| |
| if (appDisabled == OP_NEEDS_ENABLE || userDisabled == OP_NEEDS_ENABLE || |
| ds.getItemProperty(id, "userDisabled") == "true") |
| ds.setItemProperty(id, EM_R("appDisabled"), EM_L("true")); |
| else if (appDisabled == OP_NONE) |
| ds.setItemProperty(id, EM_R("appDisabled"), EM_L(OP_NEEDS_DISABLE)); |
| |
| // Don't set a new operation when there is a pending uninstall operation. |
| if (opType == OP_NEEDS_UNINSTALL) { |
| this._updateDependentItemsForID(id); |
| return; |
| } |
| |
| var operation, action; |
| // if this item is already disabled don't set a pending operation - instead |
| // immediately disable it and reset the operation type if needed. |
| if (appDisabled == OP_NEEDS_ENABLE || appDisabled == "true" || |
| userDisabled == OP_NEEDS_ENABLE || userDisabled == "true") { |
| if (opType != OP_NONE) { |
| operation = OP_NONE; |
| action = EM_ITEM_CANCEL; |
| } |
| } |
| else { |
| if (opType != OP_NEEDS_DISABLE) { |
| operation = OP_NEEDS_DISABLE; |
| action = EM_ITEM_DISABLED; |
| } |
| } |
| |
| if (action) { |
| this._setOp(id, operation); |
| this._notifyAction(id, action); |
| } |
| else { |
| ds.updateProperty(id, "satisfiesDependencies"); |
| this._updateDependentItemsForID(id); |
| } |
| }, |
| |
| /** |
| * 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. |
| * See "Note on appDisabled and userDisabled property arcs" above. |
| * @param id |
| * The ID of the item to be enabled by the user. |
| */ |
| enableItem: function(id) { |
| var ds = this.datasource; |
| var opType = ds.getItemProperty(id, "opType"); |
| var appDisabled = ds.getItemProperty(id, "appDisabled"); |
| var userDisabled = ds.getItemProperty(id, "userDisabled"); |
| |
| var operation, action; |
| // if this item is already enabled don't set a pending operation - instead |
| // immediately enable it and reset the operation type if needed. |
| if (appDisabled == OP_NONE && |
| userDisabled == OP_NEEDS_DISABLE || userDisabled == OP_NONE) { |
| if (userDisabled == OP_NEEDS_DISABLE) |
| ds.setItemProperty(id, EM_R("userDisabled"), null); |
| if (opType != OP_NONE) { |
| operation = OP_NONE; |
| action = EM_ITEM_CANCEL; |
| } |
| } |
| else { |
| if (userDisabled == "true") |
| ds.setItemProperty(id, EM_R("userDisabled"), EM_L(OP_NEEDS_ENABLE)); |
| if (opType != OP_NEEDS_ENABLE) { |
| operation = OP_NEEDS_ENABLE; |
| action = EM_ITEM_ENABLED; |
| } |
| } |
| |
| if (action) { |
| this._setOp(id, operation); |
| this._notifyAction(id, action); |
| } |
| else { |
| ds.updateProperty(id, "satisfiesDependencies"); |
| this._updateDependentItemsForID(id); |
| } |
| }, |
| |
| /** |
| * 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. |
| * See "Note on appDisabled and userDisabled property arcs" above. |
| * @param id |
| * The ID of the item to be disabled by the user. |
| */ |
| disableItem: function(id) { |
| var ds = this.datasource; |
| var opType = ds.getItemProperty(id, "opType"); |
| var appDisabled = ds.getItemProperty(id, "appDisabled"); |
| var userDisabled = ds.getItemProperty(id, "userDisabled"); |
| |
| var operation, action; |
| // if this item is already disabled don't set a pending operation - instead |
| // immediately disable it and reset the operation type if needed. |
| if (userDisabled == OP_NEEDS_ENABLE || userDisabled == "true" || |
| appDisabled == OP_NEEDS_ENABLE) { |
| if (userDisabled != "true") |
| ds.setItemProperty(id, EM_R("userDisabled"), EM_L("true")); |
| if (opType != OP_NONE) { |
| operation = OP_NONE; |
| action = EM_ITEM_CANCEL; |
| } |
| } |
| else { |
| if (userDisabled == OP_NONE) |
| ds.setItemProperty(id, EM_R("userDisabled"), EM_L(OP_NEEDS_DISABLE)); |
| if (opType != OP_NEEDS_DISABLE) { |
| operation = OP_NEEDS_DISABLE; |
| action = EM_ITEM_DISABLED; |
| } |
| } |
| |
| if (action) { |
| this._setOp(id, operation); |
| this._notifyAction(id, action); |
| } |
| else { |
| ds.updateProperty(id, "satisfiesDependencies"); |
| this._updateDependentItemsForID(id); |
| } |
| }, |
| |
| /** |
| * Determines whether an item should be disabled by the application. |
| * @param id |
| * The ID of the item to check |
| */ |
| _isUsableItem: function(id) { |
| var ds = this.datasource; |
| /* If we're not compatibility checking or if the item is compatible |
| * and if it isn't blocklisted and has all dependencies satisfied then |
| * proceed to the security check */ |
| if ((!gCheckCompatibility || ds.getItemProperty(id, "compatible") == "true") && |
| ds.getItemProperty(id, "blocklisted") == "false" && |
| ds.getItemProperty(id, "satisfiesDependencies") == "true") { |
| |
| // appManaged items aren't updated so no need to check update security. |
| if (ds.getItemProperty(id, "appManaged") == "true") |
| return true; |
| |
| /* If we are not ignoring update security then check that the item has |
| * a secure update mechanism */ |
| return (!gCheckUpdateSecurity || |
| ds.getItemProperty(id, "providesUpdatesSecurely") == "true"); |
| } |
| return false; |
| }, |
| |
| /** |
| * Sets an item's dependent items disabled state for the app based on whether |
| * its dependencies are met and the item is compatible. |
| * @param id |
| * The ID of the item whose dependent items will be checked |
| */ |
| _updateDependentItemsForID: function(id) { |
| var ds = this.datasource; |
| var dependentItems = this.getDependentItemListForID(id, true, { }); |
| for (var i = 0; i < dependentItems.length; ++i) { |
| var dependentID = dependentItems[i].id; |
| ds.updateProperty(dependentID, "satisfiesDependencies"); |
| if (this._isUsableItem(dependentID)) |
| this._appEnableItem(dependentID); |
| else |
| this._appDisableItem(dependentID); |
| } |
| }, |
| |
| /** |
| * 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, updateCheckType, listener) { |
| for (i = 0; i < itemCount; ++i) { |
| var currItem = items[i]; |
| if (!currItem) |
| throw Cr.NS_ERROR_ILLEGAL_VALUE; |
| } |
| |
| if (items.length == 0) |
| items = this.getItemList(Ci.nsIUpdateItem.TYPE_ANY, { }); |
| |
| var updater = new ExtensionItemUpdater(this); |
| updater.checkForUpdates(items, items.length, updateCheckType, listener); |
| }, |
| |
| |
| /** |
| * Checks for changes to the blocklist using the local blocklist file, |
| * application disables / enables items that have been added / removed from |
| * the blocklist, and if there are additions to the blocklist this will |
| * inform the user by displaying a list of the items added. |
| * |
| * XXXrstrong - this method is not terribly useful and was added so we can |
| * trigger this check from the additional timer used by blocklisting. |
| */ |
| checkForBlocklistChanges: function() { |
| var ds = this.datasource; |
| var items = this.getItemList(Ci.nsIUpdateItem.TYPE_ANY, { }); |
| for (var i = 0; i < items.length; ++i) { |
| var id = items[i].id; |
| ds.updateProperty(id, "blocklisted"); |
| if (this._isUsableItem(id)) |
| this._appEnableItem(id); |
| } |
| |
| items = ds.getBlocklistedItemList(null, null, Ci.nsIUpdateItem.TYPE_ANY, |
| false); |
| for (i = 0; i < items.length; ++i) |
| this._appDisableItem(items[i].id); |
| |
| // show the blocklist notification window if there are new blocklist items. |
| if (items.length > 0) |
| showBlocklistMessage(items, false); |
| }, |
| |
| /** |
| * @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 installed nsIUpdateItems of items that are dependent |
| * on another item. |
| * @param id |
| * The ID of the item that other items depend on. |
| * @param includeDisabled |
| * Whether to include disabled items in the set returned. |
| * @param countRef |
| * The XPCJS reference to the number of items returned. |
| * @returns An array of installed nsIUpdateItems that depend on the item |
| * specified by the id parameter. |
| */ |
| getDependentItemListForID: function(id, includeDisabled, countRef) { |
| return this.datasource.getDependentItemListForID(id, includeDisabled, countRef); |
| }, |
| |
| /** |
| * 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, appVersion, platformVersion, type, includeDisabled, |
| countRef) { |
| var items = this.datasource.getIncompatibleItemList(id, appVersion ? appVersion : undefined, |
| platformVersion ? platformVersion : 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); |
| }, |
| |
| /** |
| * Sorts addons of the specified type by the specified property starting from |
| * the top of their container. If the addons are already sorted then no action |
| * is performed. |
| * @param type |
| * The nsIUpdateItem type of the items to sort. |
| * @param propertyName |
| * The RDF property name used for sorting. |
| * @param isAscending |
| * true to sort ascending and false to sort descending |
| */ |
| sortTypeByProperty: function(type, propertyName, isAscending) { |
| this.datasource.sortTypeByProperty(type, propertyName, isAscending); |
| }, |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // Downloads |
| _transactions: [], |
| _downloadCount: 0, |
| _compatibilityCheckCount: 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 user has already dismissed quit request, then do nothing |
| if ((subject instanceof Ci.nsISupportsPRBool) && subject.data) |
| return; |
| |
| if (this._downloadCount > 0) { |
| // The observers will be notified again after this so set the download |
| // count to 0 to prevent this dialog from being displayed again. |
| this._downloadCount = 0; |
| var result; |
| //@line 5629 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\mozapps\extensions\src\nsExtensionManager.js.in" |
| result = this._confirmCancelDownloads(this._downloadCount, |
| "quitCancelDownloadsAlertTitle", |
| "quitCancelDownloadsAlertMsgMultiple", |
| "quitCancelDownloadsAlertMsg", |
| "dontQuitButtonWin"); |
| //@line 5641 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\mozapps\extensions\src\nsExtensionManager.js.in" |
| if (subject instanceof Ci.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 (subject instanceof Ci.nsISupportsPRBool) |
| subject.data = result; |
| } |
| }, |
| |
| /** |
| * 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 = Cc["@mozilla.org/appshell/window-mediator;1"]. |
| getService(Ci.nsIWindowMediator); |
| var win = wm.getMostRecentWindow("Extension:Manager"); |
| const nsIPromptService = Ci.nsIPromptService; |
| var ps = Cc["@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; |
| }, |
| |
| /* See nsIExtensionManager.idl */ |
| addDownloads: function(items, itemCount, manager) { |
| if (itemCount == 0) |
| throw Cr.NS_ERROR_ILLEGAL_VALUE; |
| |
| for (i = 0; i < itemCount; ++i) { |
| var currItem = items[i]; |
| if (!currItem) |
| throw Cr.NS_ERROR_ILLEGAL_VALUE; |
| } |
| |
| var ds = this.datasource; |
| // Add observers only if they aren't already added for an active download |
| if (this._downloadCount == 0) { |
| gOS.addObserver(this, "offline-requested", false); |
| gOS.addObserver(this, "quit-application-requested", false); |
| } |
| this._downloadCount += itemCount; |
| |
| var urls = []; |
| var hashes = []; |
| var txnID = Math.round(Math.random() * 100); |
| var txn = new ItemDownloadTransaction(this, txnID); |
| for (var i = 0; i < itemCount; ++i) { |
| var currItem = items[i]; |
| |
| txn.addDownload(currItem); |
| urls.push(currItem.xpiURL); |
| hashes.push(currItem.xpiHash ? currItem.xpiHash : null); |
| // if this is an update remove the update metadata to prevent it from |
| // being updated during an install. |
| if (!manager) { |
| var id = currItem.id |
| ds.setItemProperties(id, { |
| availableUpdateURL: null, |
| availableUpdateHash: null, |
| availableUpdateVersion: null, |
| availableUpdateInfo: null |
| }); |
| ds.updateProperty(id, "availableUpdateURL"); |
| ds.updateProperty(id, "updateable"); |
| } |
| var id = !manager ? PREFIX_ITEM_URI + currItem.id : currItem.xpiURL; |
| ds.updateDownloadState(id, "waiting"); |
| } |
| this._transactions.push(txn); |
| |
| if (manager) { |
| // XPIManager initiated -- let it know we're ready |
| manager.observe(txn, "xpinstall-progress", "open"); |
| } |
| else { |
| // Initiate an install from chrome |
| var xpimgr = Cc["@mozilla.org/xpinstall/install-manager;1"]. |
| createInstance(Ci.nsIXPInstallManager); |
| xpimgr.initManagerWithHashes(urls, hashes, urls.length, txn); |
| } |
| }, |
| |
| /** |
| * 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 ds = this.datasource; |
| var id = addon.id != addon.xpiURL ? PREFIX_ITEM_URI + addon.id : addon.xpiURL; |
| const nsIXPIProgressDialog = Ci.nsIXPIProgressDialog; |
| switch (state) { |
| case nsIXPIProgressDialog.DOWNLOAD_START: |
| ds.updateDownloadState(id, "downloading"); |
| for (var i = 0; i < this._installListeners.length; ++i) |
| this._installListeners[i].onDownloadStarted(addon); |
| break; |
| case nsIXPIProgressDialog.DOWNLOAD_DONE: |
| for (var i = 0; i < this._installListeners.length; ++i) |
| this._installListeners[i].onDownloadEnded(addon); |
| break; |
| case nsIXPIProgressDialog.INSTALL_START: |
| ds.updateDownloadState(id, "finishing"); |
| ds.updateDownloadProgress(id, null); |
| break; |
| case nsIXPIProgressDialog.INSTALL_DONE: |
| --this._downloadCount; |
| // From nsInstall.h |
| // SUCCESS = 0 |
| // USER_CANCELLED = -210 |
| if (value != 0 && value != -210 && id != addon.xpiURL) { |
| ds.updateDownloadState(id, "failure"); |
| ds.updateDownloadProgress(id, null); |
| } |
| transaction.removeDownload(addon.xpiURL); |
| // A successful install will be passing notifications via installItemFromFile |
| if (value != 0) { |
| for (var i = 0; i < this._installListeners.length; ++i) |
| this._installListeners[i].onInstallEnded(addon, value); |
| } |
| 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); |
| // Remove the observers when all transactions have completed. |
| if (this._transactions.length == 0) { |
| gOS.removeObserver(this, "offline-requested"); |
| gOS.removeObserver(this, "quit-application-requested"); |
| |
| // If there are no compatibility checks running then the install |
| // operations are complete. |
| if (this._compatibilityCheckCount == 0) { |
| for (var i = 0; i < this._installListeners.length; ++i) |
| this._installListeners[i].onInstallsCompleted(); |
| } |
| } |
| break; |
| } |
| } |
| // Remove any remaining downloads from this transaction |
| transaction.removeAllDownloads(); |
| break; |
| } |
| }, |
| |
| onProgress: function(addon, value, maxValue) { |
| for (var i = 0; i < this._installListeners.length; ++i) |
| this._installListeners[i].onDownloadProgress(addon, value, maxValue); |
| |
| var id = addon.id != addon.xpiURL ? PREFIX_ITEM_URI + addon.id : addon.xpiURL; |
| var progress = Math.round((value / maxValue) * 100); |
| this.datasource.updateDownloadProgress(id, progress); |
| }, |
| |
| _installListeners: [], |
| addInstallListener: function(listener) { |
| for (var i = 0; i < this._installListeners.length; ++i) { |
| if (this._installListeners[i] == listener) |
| return i; |
| } |
| this._installListeners.push(listener); |
| return this._installListeners.length - 1; |
| }, |
| |
| removeInstallListenerAt: function(index) { |
| this._installListeners.splice(index, 1); |
| }, |
| |
| /** |
| * The Extensions RDF Datasource |
| */ |
| _ds: null, |
| _ptr: 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(); |
| this._ptr = getContainer(this._ds, this._ds._itemRoot).DataSource; |
| gRDF.RegisterDataSource(this._ptr, true); |
| } |
| } |
| }, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| get datasource() { |
| this._ensureDS(); |
| return this._ds.QueryInterface(Ci.nsIRDFDataSource); |
| }, |
| |
| // nsIClassInfo |
| flags: Ci.nsIClassInfo.SINGLETON, |
| implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT, |
| getHelperForLanguage: function(language) null, |
| getInterfaces: function(count) { |
| var interfaces = [Ci.nsIExtensionManager, Ci.nsIObserver]; |
| count.value = interfaces.length; |
| return interfaces; |
| }, |
| |
| classDescription: "Extension Manager", |
| contractID: "@mozilla.org/extensions/manager;1", |
| classID: Components.ID("{8A115FAA-7DCB-4e8f-979B-5F53472F51CF}"), |
| _xpcom_categories: [{ category: "app-startup", service: true }], |
| _xpcom_factory: EmFactory, |
| QueryInterface: XPCOMUtils.generateQI([Ci.nsIExtensionManager, |
| Ci.nsITimerCallback, |
| Ci.nsIObserver, |
| Ci.nsIClassInfo]) |
| }; |
| |
| /** |
| * 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 |
| * @param manager |
| * The extension manager creating this transaction |
| * @param id |
| * The integer identifier of this transaction |
| */ |
| function ItemDownloadTransaction(manager, id) { |
| this._manager = manager; |
| this._downloads = []; |
| this.id = id; |
| } |
| 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 |
| */ |
| addDownload: function(addon) { |
| this._downloads.push({ addon: addon, waiting: true }); |
| this._manager.datasource.addDownload(addon); |
| }, |
| |
| /** |
| * 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); |
| } |
| }, |
| |
| /** |
| * 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); |
| }, |
| |
| QueryInterface: XPCOMUtils.generateQI([Ci.nsIXPIProgressDialog]) |
| }; |
| |
| /** |
| * A listener object that watches the background update check and notifies the |
| * user of any updates found. |
| */ |
| function BackgroundUpdateCheckListener(datasource) { |
| this._emDS = datasource; |
| } |
| BackgroundUpdateCheckListener.prototype = { |
| _updateCount: 0, |
| _emDS: null, |
| |
| // nsIObserver implementation |
| observe: function(aSubject, aTopic, aData) { |
| if (aTopic != "alertclickcallback") |
| return; |
| |
| var wm = Cc["@mozilla.org/appshell/window-mediator;1"]. |
| getService(Ci.nsIWindowMediator); |
| var win = wm.getMostRecentWindow("Extension:Manager"); |
| if (win) { |
| win.focus(); |
| win.showView("updates"); |
| // Don't show the update notification on next startup |
| gPref.setBoolPref(PREF_UPDATE_NOTIFYUSER, false); |
| } |
| else { |
| const EMURL = "chrome://mozapps/content/extensions/extensions.xul"; |
| const EMFEATURES = "chrome,menubar,extra-chrome,toolbar,dialog=no,resizable"; |
| |
| var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. |
| getService(Ci.nsIWindowWatcher); |
| var param = Cc["@mozilla.org/supports-array;1"]. |
| createInstance(Ci.nsISupportsArray); |
| var arg = Cc["@mozilla.org/supports-string;1"]. |
| createInstance(Ci.nsISupportsString); |
| arg.data = "updates"; |
| param.AppendElement(arg); |
| ww.openWindow(null, EMURL, null, EMFEATURES, param); |
| } |
| }, |
| |
| // nsIAddonUpdateCheckListener implementation |
| onUpdateStarted: function() { |
| }, |
| |
| onUpdateEnded: function() { |
| if (this._updateCount > 0 && Cc["@mozilla.org/alerts-service;1"]) { |
| var extensionStrings = BundleManager.getBundle(URI_EXTENSIONS_PROPERTIES); |
| var title = extensionStrings.GetStringFromName("updateNotificationTitle"); |
| var text; |
| if (this._updateCount > 1) |
| text = extensionStrings.formatStringFromName("multipleUpdateNotificationText", |
| [BundleManager.appName, this._updateCount], 2); |
| else |
| text = extensionStrings.formatStringFromName("updateNotificationText", |
| [BundleManager.appName], 1); |
| |
| try { |
| var notifier = Cc["@mozilla.org/alerts-service;1"]. |
| getService(Ci.nsIAlertsService); |
| notifier.showAlertNotification(URI_GENERIC_ICON_XPINSTALL, |
| title, text, true, "", this); |
| } |
| catch (e) { |
| LOG("Failed to retrieve alerts service, probably an unsupported " + |
| "platform - " + e); |
| } |
| } |
| }, |
| |
| onAddonUpdateStarted: function(item) { |
| }, |
| |
| onAddonUpdateEnded: function(item, status) { |
| if (status == Ci.nsIAddonUpdateCheckListener.STATUS_UPDATE) { |
| var lastupdate = this._emDS.getItemProperty(item.id, "availableUpdateVersion"); |
| if (lastupdate != item.version) { |
| gPref.setBoolPref(PREF_UPDATE_NOTIFYUSER, true); |
| this._updateCount++; |
| } |
| } |
| } |
| }; |
| |
| |
| /** |
| * 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(aEM) |
| { |
| this._emDS = aEM._ds; |
| this._em = aEM; |
| |
| getVersionChecker(); |
| } |
| |
| ExtensionItemUpdater.prototype = { |
| _emDS : null, |
| _em : null, |
| _updateCheckType : 0, |
| _items : [], |
| _listener : null, |
| |
| /* ExtensionItemUpdater |
| //@line 6180 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\mozapps\extensions\src\nsExtensionManager.js.in" |
| */ |
| checkForUpdates: function(aItems, aItemCount, aUpdateCheckType, |
| aListener) { |
| this._listener = new AddonUpdateCheckListener(aListener, this._emDS); |
| if (this._listener) |
| this._listener.onUpdateStarted(); |
| this._updateCheckType = aUpdateCheckType; |
| 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, aUpdateCheckType); |
| } |
| |
| if (this._listener && aItemCount == 0) |
| this._listener.onUpdateEnded(); |
| }, |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // 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.setTargetApplicationInfo(aLocalItem.id, |
| aRemoteItem.targetAppID, |
| aRemoteItem.minAppVersion, |
| aRemoteItem.maxAppVersion, |
| null); |
| |
| // 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._updateCheckType == Ci.nsIExtensionManager.UPDATE_SYNC_COMPATIBILITY) |
| this._emDS.setTargetApplicationInfo(aLocalItem.id, |
| aRemoteItem.targetAppID, |
| aRemoteItem.minAppVersion, |
| aRemoteItem.maxAppVersion, |
| null); |
| return false; |
| }, |
| |
| /** |
| * Checks whether a discovered update is valid for install |
| * @param aLocalItem |
| * The already installed nsIUpdateItem that the update is for |
| * @param aRemoteItem |
| * The nsIUpdateItem we are trying to update to |
| * |
| * @returns true if the item is compatible and is not blocklisted. |
| * false if the item is not compatible or is blocklisted. |
| */ |
| _isValidUpdate: function _isValidUpdate(aLocalItem, aRemoteItem) { |
| var appExtensionsVersion = (aRemoteItem.targetAppID != TOOLKIT_ID) ? |
| gApp.version : |
| gApp.platformVersion; |
| |
| var min = aRemoteItem.minAppVersion; |
| var max = aRemoteItem.maxAppVersion; |
| // Check if the update will only run on a newer version of the application. |
| if (!min || gVersionChecker.compare(appExtensionsVersion, min) < 0) |
| return false; |
| |
| // Check if the update will only run on an older version of the application. |
| if (!max || gVersionChecker.compare(appExtensionsVersion, max) > 0) |
| return false; |
| |
| if (!gBlocklist) |
| gBlocklist = Cc["@mozilla.org/extensions/blocklist;1"]. |
| getService(Ci.nsIBlocklistService); |
| if (gBlocklist.isAddonBlocklisted(aLocalItem.id, aRemoteItem.version, |
| null, null)) |
| 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); |
| } |
| } |
| }, |
| }; |
| |
| /** |
| * Replaces %...% strings in an addon url (update and updateInfo) with |
| * appropriate values. |
| * @param aItem |
| * The nsIUpdateItem representing the item |
| * @param aURI |
| * The uri to escape |
| * @param aDS |
| * The extensions datasource |
| * |
| * @returns the appropriately escaped uri. |
| */ |
| function escapeAddonURI(aItem, aURI, aDS) |
| { |
| var itemStatus = "userEnabled"; |
| if (aDS.getItemProperty(aItem.id, "userDisabled") == "true" || |
| aDS.getItemProperty(aItem.id, "userDisabled") == OP_NEEDS_ENABLE) |
| itemStatus = "userDisabled"; |
| else if (aDS.getItemProperty(aItem.id, "type") == Ci.nsIUpdateItem.TYPE_THEME) { |
| var currentSkin = gPref.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN); |
| if (aDS.getItemProperty(aItem.id, "internalName") != currentSkin) |
| itemStatus = "userDisabled"; |
| } |
| |
| if (aDS.getItemProperty(aItem.id, "compatible") == "false") |
| itemStatus += ",incompatible"; |
| if (aDS.getItemProperty(aItem.id, "blocklisted") == "true") |
| itemStatus += ",blocklisted"; |
| if (aDS.getItemProperty(aItem.id, "satisfiesDependencies") == "false") |
| itemStatus += ",needsDependencies"; |
| |
| aURI = aURI.replace(/%ITEM_ID%/g, aItem.id); |
| aURI = aURI.replace(/%ITEM_VERSION%/g, aItem.version); |
| aURI = aURI.replace(/%ITEM_MAXAPPVERSION%/g, aItem.maxAppVersion); |
| aURI = aURI.replace(/%ITEM_STATUS%/g, itemStatus); |
| aURI = aURI.replace(/%APP_ID%/g, gApp.ID); |
| aURI = aURI.replace(/%APP_VERSION%/g, gApp.version); |
| aURI = aURI.replace(/%REQ_VERSION%/g, 1); |
| aURI = aURI.replace(/%APP_OS%/g, gOSTarget); |
| aURI = aURI.replace(/%APP_ABI%/g, gXPCOMABI); |
| aURI = aURI.replace(/%APP_LOCALE%/g, gLocale); |
| |
| // Replace custom parameters (names of custom parameters must have at |
| // least 3 characters to prevent lookups for something like %D0%C8) |
| var catMan = null; |
| aURI = aURI.replace(/%(\w{3,})%/g, function(match, param) { |
| if (!catMan) { |
| catMan = Cc["@mozilla.org/categorymanager;1"]. |
| getService(Ci.nsICategoryManager); |
| } |
| |
| try { |
| var contractID = catMan.getCategoryEntry(CATEGORY_UPDATE_PARAMS, param); |
| var paramHandler = Cc[contractID]. |
| getService(Ci.nsIPropertyBag2); |
| return paramHandler.getPropertyAsAString(param); |
| } |
| catch(e) { |
| return match; |
| } |
| }); |
| |
| // escape() does not properly encode + symbols in any embedded FVF strings. |
| return aURI.replace(/\+/g, "%2B"); |
| } |
| |
| function RDFItemUpdater(aUpdater) { |
| this._updater = aUpdater; |
| } |
| |
| RDFItemUpdater.prototype = { |
| _updater : null, |
| _updateCheckType : 0, |
| _item : null, |
| |
| checkForUpdates: function(aItem, aUpdateCheckType) { |
| // A preference setting can disable updating for this item |
| try { |
| if (!gPref.getBoolPref(PREF_EM_ITEM_UPDATE_ENABLED.replace(/%UUID%/, aItem.id))) { |
| var status = Ci.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 = Ci.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) { |
| var status = Ci.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 managed independently |
| if (installLocation && installLocation.itemIsManagedIndependently(aItem.id)) { |
| var status = Ci.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 ((aUpdateCheckType == Ci.nsIExtensionManager.UPDATE_CHECK_NEWVERSION) && |
| (!installLocation || !installLocation.canAccess)) { |
| var status = Ci.nsIAddonUpdateCheckListener.STATUS_READ_ONLY; |
| this._updater.checkForDone(aItem, status); |
| return; |
| } |
| |
| this._updateCheckType = aUpdateCheckType; |
| 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), |
| Ci.nsIPrefLocalizedString).data; |
| } |
| catch (e) { } |
| if (!dsURI) |
| dsURI = aItem.updateRDF; |
| if (!dsURI) |
| dsURI = gPref.getCharPref(PREF_UPDATE_DEFAULT_URL); |
| |
| dsURI = escapeAddonURI(aItem, dsURI, emDS); |
| |
| // 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, |
| Ci.nsIAddonUpdateCheckListener.STATUS_FAILURE); |
| return; |
| } |
| |
| LOG("RDFItemUpdater:checkForUpdates sending a request to server for: " + |
| uri.spec + ", item = " + aItem.objectSource); |
| |
| var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. |
| createInstance(Ci.nsIXMLHttpRequest); |
| request.open("GET", uri.spec, true); |
| request.channel.notificationCallbacks = new BadCertHandler(); |
| request.overrideMimeType("text/xml"); |
| request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_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; |
| try { |
| checkCert(request.channel); |
| } |
| catch (e) { |
| // This may be overly restrictive in two cases: corporate installations |
| // with a corporate update server using an in-house CA cert (installed |
| // but not "built-in") and lone developers hosting their updates on a |
| // site with a self-signed cert (permanently accepted, otherwise the |
| // BadCertHandler would prevent getting this far). Update checks will |
| // fail in both these scenarios. |
| // How else can we protect the vast majority of updates served from AMO |
| // from the spoofing attack described in bug 340198 while allowing those |
| // other cases? A "hackme" pref? Domain-control certs are cheap, getting |
| // one should not be a barrier in either case. |
| LOG("RDFItemUpdater::onXMLLoad: " + e); |
| this._updater.checkForDone(aItem, |
| Ci.nsIAddonUpdateCheckListener.STATUS_FAILURE); |
| return; |
| } |
| var responseXML = request.responseXML; |
| |
| // If the item does not have an update RDF and returns an error it is not |
| // treated as a failure since all items without an updateURL are checked |
| // for updates on AMO even if they are not hosted there. |
| if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR || |
| (request.status != 200 && request.status != 0)) { |
| this._updater.checkForDone(aItem, (aItem.updateRDF ? Ci.nsIAddonUpdateCheckListener.STATUS_FAILURE : |
| Ci.nsIAddonUpdateCheckListener.STATUS_NONE)); |
| return; |
| } |
| |
| var rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"]. |
| createInstance(Ci.nsIRDFXMLParser) |
| var ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]. |
| createInstance(Ci.nsIRDFDataSource); |
| 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(Ci.nsIRequest); |
| status = request.status; |
| } |
| // this can fail when a network connection is not present. |
| try { |
| var statusText = request.statusText; |
| } |
| catch (e) { |
| status = 0; |
| } |
| // 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, |
| Ci.nsIAddonUpdateCheckListener.STATUS_FAILURE); |
| }, |
| |
| onDatasourceLoaded: function(aDatasource, aLocalItem) { |
| /* |
| //@line 6578 "e:\builds\tinderbox\XR-Trunk\WINNT_5.2_Depend\mozilla\toolkit\mozapps\extensions\src\nsExtensionManager.js.in" |
| */ |
| 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)"); |
| } |
| |
| // If we have an update key then the update manifest must be signed |
| if (aLocalItem.updateKey) { |
| var extensionRes = gRDF.GetResource(getItemPrefix(aLocalItem.type) + aLocalItem.id); |
| LOG(extensionRes.Value); |
| var signature = this._getPropertyFromResource(aDatasource, extensionRes, "signature", null); |
| if (signature) { |
| var serializer = new RDFSerializer(); |
| try { |
| var updateString = serializer.serializeResource(aDatasource, extensionRes); |
| var verifier = Cc["@mozilla.org/security/datasignatureverifier;1"]. |
| getService(Ci.nsIDataSignatureVerifier); |
| try { |
| if (!verifier.verifyData(updateString, signature, aLocalItem.updateKey)) { |
| LOG("RDFItemUpdater:onDatasourceLoaded: Update manifest for " + |
| aLocalItem.id + " failed signature check."); |
| this._updater.checkForDone(aLocalItem, Ci.nsIAddonUpdateCheckListener.STATUS_FAILURE); |
| return; |
| } |
| } |
| catch (e) { |
| LOG("RDFItemUpdater:onDatasourceLoaded: Failed to verify signature for " + |
| aLocalItem.id + ". This indicates a malformed update key or signature."); |
| this._updater.checkForDone(aLocalItem, Ci.nsIAddonUpdateCheckListener.STATUS_FAILURE); |
| return; |
| } |
| } |
| catch (e) { |
| LOG("RDFItemUpdater:onDatasourceLoaded: Failed to generate signature " + |
| "string for " + aLocalItem.id + ". Serializer threw " + e); |
| this._updater.checkForDone(aLocalItem, Ci.nsIAddonUpdateCheckListener.STATUS_FAILURE); |
| return; |
| } |
| } |
| else { |
| LOG("RDFItemUpdater:onDatasourceLoaded: Update manifest for " + |
| aLocalItem.id + " did not contain a signature."); |
| this._updater.checkForDone(aLocalItem, Ci.nsIAddonUpdateCheckListener.STATUS_FAILURE); |
| return; |
| } |
| } |
| /* If there is no updateKey either the update was over SSL, or it is an old |
| * addon that we are allowing a grace update. */ |
| |
| // Parse the response RDF |
| var newerItem, sameItem; |
| |
| // Firefox 1.0PR+ update.rdf format |
| if (this._updateCheckType == Ci.nsIExtensionManager.UPDATE_CHECK_NEWVERSION) { |
| // 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. |
| newerItem = this._parseV20UpdateInfo(aDatasource, aLocalItem, |
| Ci.nsIExtensionManager.UPDATE_CHECK_NEWVERSION); |
| |
| if (newerItem) { |
| ++this._updater._updateCount; |
| LOG("RDFItemUpdater:onDatasourceLoaded: Found a newer version of this item:\r\n" + |
| newerItem.objectSource); |
| } |
| } |
| |
| // Now look for updated version compatibility metadata for the currently |
| // installed version... |
| sameItem = this._parseV20UpdateInfo(aDatasource, aLocalItem, |
| Ci.nsIExtensionManager.UPDATE_CHECK_COMPATIBILITY); |
| |
| if (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 |
| LOG("RDFItemUpdater:onDatasourceLoaded: Found info about the installed\r\n" + |
| "version of this item: " + sameItem.objectSource); |
| } |
| var item = null, status = Ci.nsIAddonUpdateCheckListener.STATUS_NONE; |
| if (this._updateCheckType == Ci.nsIExtensionManager.UPDATE_CHECK_NEWVERSION |
| && newerItem) { |
| item = newerItem; |
| status = Ci.nsIAddonUpdateCheckListener.STATUS_UPDATE; |
| } |
| else if (sameItem) { |
| item = sameItem; |
| status = Ci.nsIAddonUpdateCheckListener.STATUS_VERSIONINFO; |
| } |
| else { |
| item = aLocalItem; |
| status = Ci.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); |
| }, |
| |
| // 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 Cr.NS_ERROR_FAILURE; |
| } |
| catch (e) { |
| // XXXben show console message "aProperty" not found on aSourceResource. |
| return null; |
| } |
| return rv; |
| }, |
| |
| /** |
| * Parses the Firefox 1.0RC1+ update manifest format looking for new versions |
| * of updated compatibility information about the given add-on. |
| * @param aDataSource |
| * The update manifest's datasource |
| * @param aLocalItem |
| * The nsIUpdateItem representing the add-on being checked for updates. |
| * @param aUpdateCheckType |
| * The type of update check being performed. See the constants in |
| * nsIExtensionManager |
| * @returns An nsIUpdateItem holding the update's information if a valid |
| * update is found or null if not. |
| */ |
| _parseV20UpdateInfo: function(aDataSource, aLocalItem, aUpdateCheckType) { |
| 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(Ci.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 null; |
| } |
| |
| // Track the newest update found |
| var updatedItem = null; |
| |
| var cu = Cc["@mozilla.org/rdf/container-utils;1"]. |
| getService(Ci.nsIRDFContainerUtils); |
| if (cu.IsContainer(aDataSource, updates)) { |
| var ctr = getContainer(aDataSource, updates); |
| |
| var versions = ctr.GetElements(); |
| while (versions.hasMoreElements()) { |
| // There are two different methodologies for collecting version |
| // information depending on whether or not we've been invoked in |
| // "version updates only" mode or "version+newest" mode. |
| var version = versions.getNext().QueryInterface(Ci.nsIRDFResource); |
| var foundItem = this._parseV20Update(aDataSource, version, aLocalItem, |
| updatedItem ? updatedItem.version : aLocalItem.version, |
| aUpdateCheckType); |
| if (foundItem) { |
| // When not checking for new versions we can bail out on the first |
| // result. |
| if (aUpdateCheckType) |
| return foundItem; |
| updatedItem = foundItem; |
| } |
| } |
| } |
| return updatedItem; |
| }, |
| |
| /** |
| * Parses a single version's update entry looking for the best matching |
| * targetApplication entry. |
| * @param aDataSource |
| * The update manifest's datasource |
| * @param aUpdateResource |
| * The nsIRDFResource of the update entry. |
| * @param aLocalItem |
| * The nsIUpdateItem representing the add-on being checked for updates. |
| * @param aNewestVersionFound |
| * When checking for new versions holds the newest version of this |
| * add-on that we know about. Otherwise holds the current version. |
| * @param aUpdateCheckType |
| * The type of update check being performed. See the constants in |
| * nsIExtensionManager |
| * @returns An nsIUpdateItem holding the update's information if a valid |
| * update is found or null if not. |
| */ |
| _parseV20Update: function(aDataSource, aUpdateResource, aLocalItem, aNewestVersionFound, aUpdateCheckType) { |
| var version = this._getPropertyFromResource(aDataSource, aUpdateResource, |
| "version", aLocalItem); |
| /* If we are looking for new versions then test whether this discovered |
| * version is greater than any previously found update. Otherwise check |
| * if this update is for the same version as we have installed. */ |
| var result = gVersionChecker.compare(version, aNewestVersionFound); |
| if (aUpdateCheckType == Ci.nsIExtensionManager.UPDATE_CHECK_NEWVERSION ? result <= 0 : result != 0) |
| return null; |
| |
| var taArc = gRDF.GetResource(EM_NS("targetApplication")); |
| var targetApps = aDataSource.GetTargets(aUpdateResource, taArc, true); |
| |
| // Track the best update we have found so far |
| var newestUpdateItem = null; |
| while (targetApps.hasMoreElements()) { |
| var targetApp = targetApps.getNext().QueryInterface(Ci.nsIRDFResource); |
| var appID = this._getPropertyFromResource(aDataSource, targetApp, "id", aLocalItem); |
| if (appID != gApp.ID && appID != TOOLKIT_ID) |
| continue; |
| |
| var updateLink = this._getPropertyFromResource(aDataSource, targetApp, "updateLink", aLocalItem); |
| var updateHash = this._getPropertyFromResource(aDataSource, targetApp, "updateHash", aLocalItem); |
| if (aUpdateCheckType == Ci.nsIExtensionManager.UPDATE_CHECK_NEWVERSION) { |
| // New version information is useless without a link to get it from |
| if (!updateLink) |
| continue; |
| |
| /* If the update link is non-ssl and we do not have a hash or the hash |
| * is of an insecure nature then we must ignore this update. Bypass |
| * this if not checking update security. Currently we only consider |
| * the sha hashing algorithms as secure. */ |
| if (gCheckUpdateSecurity && updateLink.substring(0, 6) != "https:" && |
| (!updateHash || updateHash.substring(0, 3) != "sha")) { |
| LOG("RDFItemUpdater:_parseV20Update: Update for " + aLocalItem.id + |
| " at " + updateLink + " ignored because it is insecure. updateLink " + |
| " must be a https url or an updateHash must be specified."); |
| continue; |
| } |
| } |
| |
| var updatedItem = makeItem(aLocalItem.id, |
| version, |
| aLocalItem.installLocationKey, |
| this._getPropertyFromResource(aDataSource, targetApp, "minVersion", aLocalItem), |
| this._getPropertyFromResource(aDataSource, targetApp, "maxVersion", aLocalItem), |
| aLocalItem.name, |
| updateLink, |
| updateHash, |
| "", /* Icon URL */ |
| "", /* RDF Update URL */ |
| "", /* Update Key */ |
| aLocalItem.type, |
| appID); |
| |
| if (this._updater._isValidUpdate(aLocalItem, updatedItem)) { |
| if (aUpdateCheckType == Ci.nsIExtensionManager.UPDATE_CHECK_NEWVERSION) { |
| var infourl = this._getPropertyFromResource(aDataSource, targetApp, |
| "updateInfoURL"); |
| if (infourl) |
| infourl = EM_L(infourl); |
| this._updater._emDS.setItemProperty(aLocalItem.id, |
| EM_R("availableUpdateInfo"), |
| infourl); |
| } |
| if (appID == gApp.ID) { |
| // App takes precedence over toolkit. If we found the app, bail out. |
| return updatedItem; |
| } |
| newestUpdateItem = updatedItem; |
| } |
| } |
| return newestUpdateItem; |
| } |
| }; |
| |
| /** |
| * A serialisation method for RDF data that produces an identical string |
| * provided that the RDF assertions match. |
| * The serialisation is not complete, only assertions stemming from a given |
| * resource are included, multiple references to the same resource are not |
| * permitted, and the RDF prolog and epilog are not included. |
| * RDF Blob and Date literals are not supported. |
| */ |
| function RDFSerializer() |
| { |
| this.cUtils = Cc["@mozilla.org/rdf/container-utils;1"]. |
| getService(Ci.nsIRDFContainerUtils); |
| this.resources = []; |
| } |
| |
| RDFSerializer.prototype = { |
| INDENT: " ", // The indent used for pretty-printing |
| resources: null, // Array of the resources that have been found |
| |
| /** |
| * Escapes characters from a string that should not appear in XML. |
| * @param string The string to be escaped |
| * @returns a string with all characters invalid in XML character data |
| * converted to entity references. |
| */ |
| escapeEntities: function(string) |
| { |
| string = string.replace(/&/g, "&"); |
| string = string.replace(/</g, "<"); |
| string = string.replace(/>/g, ">"); |
| string = string.replace(/"/g, """); |
| return string; |
| }, |
| |
| /** |
| * Serializes all the elements of an RDF container. |
| * @param ds The datasource holding the data |
| * @param container The RDF container to output the child elements of |
| * @param indent The current level of indent for pretty-printing |
| * @returns a string containing the serialized elements. |
| */ |
| serializeContainerItems: function(ds, container, indent) |
| { |
| var result = ""; |
| var items = container.GetElements(); |
| while (items.hasMoreElements()) { |
| var item = items.getNext().QueryInterface(Ci.nsIRDFResource); |
| result += indent + "<RDF:li>\n" |
| result += this.serializeResource(ds, item, indent + this.INDENT); |
| result += indent + "</RDF:li>\n" |
| } |
| return result; |
| }, |
| |
| /** |
| * Serializes all em:* (see EM_NS) properties of an RDF resource except for |
| * the em:signature property. As this serialization is to be compared against |
| * the manifest signature it cannot contain the em:signature property itself. |
| * @param ds The datasource holding the data |
| * @param resource The RDF resource to output the properties of |
| * @param indent The current level of indent for pretty-printing |
| * @returns a string containing the serialized properties. |
| */ |
| serializeResourceProperties: function(ds, resource, indent) |
| { |
| var result = ""; |
| var items = []; |
| var arcs = ds.ArcLabelsOut(resource); |
| while (arcs.hasMoreElements()) { |
| var arc = arcs.getNext().QueryInterface(Ci.nsIRDFResource); |
| if (arc.ValueUTF8.substring(0, PREFIX_NS_EM.length) != PREFIX_NS_EM) |
| continue; |
| var prop = arc.ValueUTF8.substring(PREFIX_NS_EM.length); |
| if (prop == "signature") |
| continue; |
| |
| var targets = ds.GetTargets(resource, arc, true); |
| while (targets.hasMoreElements()) { |
| var target = targets.getNext(); |
| if (target instanceof Ci.nsIRDFResource) { |
| var item = indent + "<em:" + prop + ">\n"; |
| item += this.serializeResource(ds, target, indent + this.INDENT); |
| item += indent + "</em:" + prop + ">\n"; |
| items.push(item); |
| } |
| else if (target instanceof Ci.nsIRDFLiteral) { |
| items.push(indent + "<em:" + prop + ">" + this.escapeEntities(target.Value) + "</em:" + prop + ">\n"); |
| } |
| else if (target instanceof Ci.nsIRDFInt) { |
| items.push(indent + "<em:" + prop + " NC:parseType=\"Integer\">" + target.Value + "</em:" + prop + ">\n"); |
| } |
| else { |
| throw new Error("Cannot serialize unknown literal type"); |
| } |
| } |
| } |
| items.sort(); |
| result += items.join(""); |
| return result; |
| }, |
| |
| /** |
| * Recursively serializes an RDF resource and all resources it links to. |
| * This will only output EM_NS properties and will ignore any em:signature |
| * property. |
| * @param ds The datasource holding the data |
| * @param resource The RDF resource to serialize |
| * @param indent The current level of indent for pretty-printing. |
| * Leave undefined for no indent |
| * @returns a string containing the serialized resource. |
| * @throws if the RDF data contains multiple references to the same resource. |
| */ |
| serializeResource: function(ds, resource, indent) |
| { |
| if (this.resources.indexOf(resource) != -1 ) { |
| // We cannot output multiple references to the same resource. |
| throw new Error("Cannot serialize multiple references to "+resource.Value); |
| } |
| if (indent === undefined) |
| indent = ""; |
| |
| this.resources.push(resource); |
| var container = null; |
| var type = "Description"; |
| if (this.cUtils.IsSeq(ds, resource)) { |
| type = "Seq"; |
| container = this.cUtils.MakeSeq(ds, resource); |
| } |
| else if (this.cUtils.IsAlt(ds, resource)) { |
| type = "Alt"; |
| container = this.cUtils.MakeAlt(ds, resource); |
| } |
| else if (this.cUtils.IsBag(ds, resource)) { |
| type = "Bag"; |
| container = this.cUtils.MakeBag(ds, resource); |
| } |
| |
| var result = indent + "<RDF:" + type; |
| if (!gRDF.IsAnonymousResource(resource)) |
| result += " about=\"" + this.escapeEntities(resource.ValueUTF8) + "\""; |
| result += ">\n"; |
| |
| if (container) |
| result += this.serializeContainerItems(ds, container, indent + this.INDENT); |
| |
| result += this.serializeResourceProperties(ds, resource, indent + this.INDENT); |
| |
| result += indent + "</RDF:" + type + ">\n"; |
| return result; |
| } |
| } |
| |
| /** |
| * 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); |
| } |
| ExtensionsDataSource.prototype = { |
| _inner : null, |
| _em : null, |
| _itemRoot : null, |
| _defaultTheme : null, |
| |
| /** |
| * Determines if an item's dependencies are satisfied. An item's dependencies |
| * are satisifed when all items specified in the item's em:requires arc are |
| * installed, enabled, and the version is compatible based on the em:requires |
| * minVersion and maxVersion. |
| * @param id |
| * The ID of the item |
| * @returns true if the item's dependencies are satisfied. |
| * false if the item's dependencies are not satisfied. |
| */ |
| satisfiesDependencies: function(id) { |
| var ds = this._inner; |
| var itemResource = getResourceForID(id); |
| var targets = ds.GetTargets(itemResource, EM_R("requires"), true); |
| if (!targets.hasMoreElements()) |
| return true; |
| |
| getVersionChecker(); |
| var idRes = EM_R("id"); |
| var minVersionRes = EM_R("minVersion"); |
| var maxVersionRes = EM_R("maxVersion"); |
| while (targets.hasMoreElements()) { |
| var target = targets.getNext().QueryInterface(Ci.nsIRDFResource); |
| var dependencyID = stringData(ds.GetTarget(target, idRes, true)); |
| var version = null; |
| version = this.getItemProperty(dependencyID, "version"); |
| if (version) { |
| var opType = this.getItemProperty(dependencyID, "opType"); |
| if (opType == OP_NEEDS_DISABLE || opType == OP_NEEDS_UNINSTALL) |
| return false; |
| |
| if (this.getItemProperty(dependencyID, "userDisabled") == "true" || |
| this.getItemProperty(dependencyID, "appDisabled") == "true" || |
| this.getItemProperty(dependencyID, "userDisabled") == OP_NEEDS_DISABLE || |
| this.getItemProperty(dependencyID, "appDisabled") == OP_NEEDS_DISABLE) |
| return false; |
| |
| var minVersion = stringData(ds.GetTarget(target, minVersionRes, true)); |
| var maxVersion = stringData(ds.GetTarget(target, maxVersionRes, true)); |
| var compatible = (gVersionChecker.compare(version, minVersion) >= 0 && |
| gVersionChecker.compare(version, maxVersion) <= 0); |
| if (!compatible) |
| return false; |
| } |
| else { |
| return false; |
| } |
| } |
| |
| return true; |
| }, |
| |
| /** |
| * 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 appVersion |
| * The version of the application we are checking for compatibility |
| * against. If this parameter is undefined, the version of the running |
| * application is used. |
| * @param platformVersion |
| * The version of the toolkit to check compatibility against |
| * @returns true if the item is compatible with this version of the |
| * application, false, otherwise. |
| */ |
| isCompatible: function (datasource, source, appVersion, platformVersion) { |
| // The Default Theme is always compatible. |
| if (source.EqualsNode(this._defaultTheme)) |
| return true; |
| |
| var appID = gApp.ID; |
| if (appVersion === undefined) |
| appVersion = gApp.version; |
| if (platformVersion === undefined) |
| var platformVersion = gApp.platformVersion; |
| |
| var targets = datasource.GetTargets(source, EM_R("targetApplication"), true); |
| var idRes = EM_R("id"); |
| var minVersionRes = EM_R("minVersion"); |
| var maxVersionRes = EM_R("maxVersion"); |
| var versionChecker = getVersionChecker(); |
| var rv = false; |
| while (targets.hasMoreElements()) { |
| var targetApp = targets.getNext().QueryInterface(Ci.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) { |
| rv = (versionChecker.compare(appVersion, minVersion) >= 0) && |
| (versionChecker.compare(appVersion, maxVersion) <= 0); |
| return rv; // App takes precedence over toolkit. |
| } |
| |
| if (id == TOOLKIT_ID) { |
| rv = (versionChecker.compare(platformVersion, minVersion) >= 0) && |
| (versionChecker.compare(platformVersion, maxVersion) <= 0); |
| // Keep looping, in case the app id is later. |
| } |
| } |
| return rv; |
| }, |
| |
| /** |
| * 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 platformVersion |
| * The version of the toolkit to check compatibility 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, platformVersion, |
| desiredType, includeDisabled) { |
| var items = []; |
| var ctr = getContainer(this._inner, this._itemRoot); |
| var elements = ctr.GetElements(); |
| while (elements.hasMoreElements()) { |
| var item = elements.getNext().QueryInterface(Ci.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, "isDisabled") == "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, platformVersion)) |
| items.push(this.getItemForID(id)); |
| } |
| return items; |
| }, |
| |
| /** |
| * Retrieves a list of items that will be blocklisted by the application for |
| * a specific application or toolkit version. |
| * @param appVersion |
| * The Version of the application to check the blocklist against. |
| * @param platformVersion |
| * The Version of the toolkit to check the blocklist against. |
| * @param desiredType |
| * The nsIUpdateItem type of items to look for |
| * @param includeAppDisabled |
| * Whether or not items that are or are already set to be disabled |
| * by the app on next restart should be included in the set returned |
| * @returns An array of nsIUpdateItems that are blocklisted with the application |
| * or toolkit version supplied. |
| */ |
| getBlocklistedItemList: function(appVersion, platformVersion, desiredType, |
| includeAppDisabled) { |
| if (!gBlocklist) |
| gBlocklist = Cc["@mozilla.org/extensions/blocklist;1"]. |
| getService(Ci.nsIBlocklistService); |
| var items = []; |
| var ctr = getContainer(this._inner, this._itemRoot); |
| var elements = ctr.GetElements(); |
| while (elements.hasMoreElements()) { |
| var item = elements.getNext().QueryInterface(Ci.nsIRDFResource); |
| var id = stripPrefix(item.Value, PREFIX_ITEM_URI); |
| var type = this.getItemProperty(id, "type"); |
| |
| if (!includeAppDisabled && |
| (this.getItemProperty(id, "appDisabled") == "true" || |
| this.getItemProperty(id, "appDisabled") == OP_NEEDS_DISABLE)) |
| continue; |
| |
| var version = this.getItemProperty(id, "version"); |
| if (type != -1 && (type & desiredType) && |
| gBlocklist.isAddonBlocklisted(id, version, appVersion, platformVersion)) |
| 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(Ci.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; |
| }, |
| |
| /** |
| * Retrieves a list of installed nsIUpdateItems of items that are dependent |
| * on another item. |
| * @param id |
| * The ID of the item that other items depend on. |
| * @param includeDisabled |
| * Whether to include disabled items in the set returned. |
| * @param countRef |
| * The XPCJS reference to the number of items returned. |
| * @returns An array of installed nsIUpdateItems that depend on the item |
| * specified by the id parameter. |
| */ |
| getDependentItemListForID: function(id, includeDisabled, countRef) { |
| var items = []; |
| var ds = this._inner; |
| var ctr = getContainer(this, this._itemRoot); |
| var elements = ctr.GetElements(); |
| while (elements.hasMoreElements()) { |
| var e = elements.getNext().QueryInterface(Ci.nsIRDFResource); |
| var dependentID = stripPrefix(e.Value, PREFIX_ITEM_URI); |
| var targets = ds.GetTargets(e, EM_R("requires"), true); |
| var idRes = EM_R("id"); |
| while (targets.hasMoreElements()) { |
| var target = targets.getNext().QueryInterface(Ci.nsIRDFResource); |
| var dependencyID = stringData(ds.GetTarget(target, idRes, true)); |
| if (dependencyID == id) { |
| if (!includeDisabled && this.getItemProperty(dependentID, "isDisabled") == "true") |
| continue; |
| items.push(this.getItemForID(dependentID)); |
| break; |
| } |
| } |
| } |
| countRef.value = items.length; |
| 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) { |
| if (!this.visibleItems[id]) |
| return null; |
| |
| 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, "updateKey"), |
| this.getItemProperty(id, "type"), |
| targetAppInfo ? targetAppInfo.appID : gApp.ID); |
| }, |
| |
| /** |
| * 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); |
| }, |
| |
| /** |
| * 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); |
| var outData = null; |
| while (targetApps.hasMoreElements()) { |
| var targetApp = targetApps.getNext(); |
| if (targetApp instanceof Ci.nsIRDFResource) { |
| try { |
| var foundAppID = stringData(this._inner.GetTarget(targetApp, EM_R("id"), true)); |
| // Different target application? |
| if (foundAppID != appID && foundAppID != TOOLKIT_ID) |
| continue; |
| var updatedMinVersion = this._inner.GetTarget(targetApp, EM_R("updatedMinVersion"), true); |
| var updatedMaxVersion = this._inner.GetTarget(targetApp, EM_R("updatedMaxVersion"), true); |
| if (updatedMinVersion && updatedMaxVersion) |
| outData = { id : id, |
| targetAppID : foundAppID, |
| minVersion : stringData(updatedMinVersion), |
| maxVersion : stringData(updatedMaxVersion) }; |
| if (foundAppID == appID) |
| return outData; |
| } |
| catch (e) { |
| continue; |
| } |
| } |
| } |
| return outData; |
| }, |
| |
| /** |
| * 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 targetAppID |
| * The target application ID used for checking compatibility for this item. |
| * @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 |
| * |
| * @note Add-ons can specify a targetApplication id of toolkit@mozilla.org in |
| * their install manifest for compatibility with all apps using a |
| * specific release of the toolkit. |
| */ |
| setUpdatedTargetAppInfo: function(id, targetAppID, 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 Ci.nsIRDFResource) { |
| var foundAppID = stringData(this._inner.GetTarget(targetApp, EM_R("id"), true)); |
| // Different target application? |
| if (foundAppID != targetAppID) |
| 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: |
| * "appID" The target application ID used for checking |
| * compatibility for this item. |
| * "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) { |
| var appID = gApp.ID; |
| // The default theme is always compatible. |
| if (getResourceForID(id).EqualsNode(this._defaultTheme)) { |
| var ver = gApp.version; |
| return { appID: appID, minVersion: ver, maxVersion: ver }; |
| } |
| |
| 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); |
| var outData = null; |
| while (targetApps.hasMoreElements()) { |
| var targetApp = targetApps.getNext(); |
| if (targetApp instanceof Ci.nsIRDFResource) { |
| try { |
| var foundAppID = stringData(datasource.GetTarget(targetApp, EM_R("id"), true)); |
| // Different target application? |
| if (foundAppID != appID && foundAppID != TOOLKIT_ID) |
| continue; |
| |
| outData = { appID: foundAppID, |
| minVersion: stringData(datasource.GetTarget(targetApp, EM_R("minVersion"), true)), |
| maxVersion: stringData(datasource.GetTarget(targetApp, EM_R("maxVersion"), true)) }; |
| if (foundAppID == appID) |
| return outData; |
| } |
| catch (e) { |
| continue; |
| } |
| } |
| } |
| return outData; |
| }, |
| |
| /** |
| * 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 targetAppID |
| * The target application ID used for checking compatibility for this |
| * item. |
| * @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 look up target application info in |
| * |
| * @note Add-ons can specify a targetApplication id of toolkit@mozilla.org in |
| * their install manifest for compatibility with all apps using a |
| * specific release of the toolkit. |
| */ |
| setTargetApplicationInfo: function(id, targetAppID, 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 Ci.nsIRDFResource) { |
| var foundAppID = stringData(targetDataSource.GetTarget(targetApp, EM_R("id"), true)); |
| // Different target application? |
| if (foundAppID != targetAppID) |
| 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(); |
| }, |
| |
| /** |
| * Sets one or more properties for an item. |
| * @param id |
| * The ID of the item |
| * @param properties |
| * A JS object which maps properties to values. |
| */ |
| setItemProperties: function (id, properties) { |
| var item = getResourceForID(id); |
| for (var key in properties) |
| this._setProperty(this._inner, item, EM_R(key), properties[key]); |
| 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); |
| this.visibleItems[id] = null; |
| }, |
| |
| /** |
| * 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 localized properties from an install manifest to the datasource |
| * |
| * @param installManifest |
| * The Install Manifest datasource we are copying from |
| * @param source |
| * The source resource of the localized properties |
| * @param target |
| * The target resource to store the localized properties |
| */ |
| _addLocalizedMetadata: function(installManifest, sourceRes, targetRes) |
| { |
| var singleProps = ["name", "description", "creator", "homepageURL"]; |
| |
| for (var i = 0; i < singleProps.length; ++i) { |
| var property = EM_R(singleProps[i]); |
| var literal = installManifest.GetTarget(sourceRes, 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 = ["developer", "translator", "contributor"]; |
| for (var i = 0; i < manyProps.length; ++i) { |
| var property = EM_R(manyProps[i]); |
| var literals = installManifest.GetTargets(sourceRes, property, true); |
| |
| var oldValues = this._inner.GetTargets(targetRes, property, true); |
| while (oldValues.hasMoreElements()) { |
| var oldValue = oldValues.getNext().QueryInterface(Ci.nsIRDFNode); |
| this._inner.Unassert(targetRes, property, oldValue); |
| } |
| while (literals.hasMoreElements()) { |
| var literal = literals.getNext().QueryInterface(Ci.nsIRDFNode); |
| this._inner.Assert(targetRes, property, literal, true); |
| } |
| } |
| |
| }, |
| |
| /** |
| * 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) { |
| var targetRes = getResourceForID(id); |
| // Remove any temporary assertions used for the install process |
| this._setProperty(this._inner, targetRes, EM_R("newVersion"), null); |
| // Copy the assertions over from the source datasource. |
| // Assert properties with single values |
| var singleProps = ["version", "updateURL", "updateService", "optionsURL", |
| "aboutURL", "iconURL", "internalName", "updateKey"]; |
| |
| // 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); |
| } |
| |
| var localizedProp = EM_R("localized"); |
| var localeProp = EM_R("locale"); |
| // Remove old localized properties |
| var oldValues = this._inner.GetTargets(targetRes, localizedProp, true); |
| while (oldValues.hasMoreElements()) { |
| var oldValue = oldValues.getNext().QueryInterface(Ci.nsIRDFNode); |
| this._cleanResource(oldValue); |
| this._inner.Unassert(targetRes, localizedProp, oldValue); |
| } |
| // Add each localized property |
| var localizations = installManifest.GetTargets(gInstallManifestRoot, localizedProp, true); |
| while (localizations.hasMoreElements()) { |
| var localization = localizations.getNext().QueryInterface(Ci.nsIRDFResource); |
| var anon = gRDF.GetAnonymousResource(); |
| var literals = installManifest.GetTargets(localization, localeProp, true); |
| while (literals.hasMoreElements()) { |
| var literal = literals.getNext().QueryInterface(Ci.nsIRDFNode); |
| this._inner.Assert(anon, localeProp, literal, true); |
| } |
| this._addLocalizedMetadata(installManifest, localization, anon); |
| this._inner.Assert(targetRes, localizedProp, anon, true); |
| } |
| // Add the fallback properties |
| this._addLocalizedMetadata(installManifest, gInstallManifestRoot, targetRes); |
| |
| // 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(Ci.nsIRDFResource); |
| this._cleanResource(oldVersionInfo); |
| this._inner.Unassert(targetRes, property, oldVersionInfo); |
| } |
| while (newVersionInfos.hasMoreElements()) { |
| var newVersionInfo = newVersionInfos.getNext().QueryInterface(Ci.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.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", "localized"]; |
| 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(Ci.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(Ci.nsIRDFResource); |
| var targets = this._inner.GetTargets(resource, arc, true); |
| while (targets.hasMoreElements()) { |
| var value = targets.getNext().QueryInterface(Ci.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); |
| this._updateProperty(item, property); |
| }, |
| |
| /** |
| * 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). This allows updating |
| * properties for download items which don't have the em item prefix in there |
| ( resource value. In most instances updateProperty should be used. |
| * @param item |
| * The item to update the property for. |
| * @param property |
| * The property (less EM_NS) to update. |
| */ |
| _updateProperty: function(item, property) { |
| if (item) { |
| var propertyResource = EM_R(property); |
| var value = this.GetTarget(item, propertyResource, true); |
| for (var i = 0; i < this._observers.length; ++i) { |
| if (value) |
| this._observers[i].onChange(this, item, propertyResource, |
| EM_L(""), value); |
| else |
| this._observers[i].onUnassert(this, item, propertyResource, |
| EM_L("")); |
| } |
| } |
| }, |
| |
| /** |
| * 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(); |
| }, |
| |
| /** |
| * Sorts addons of the specified type by the specified property starting from |
| * the top of their container. If the addons are already sorted then no action |
| * is performed. |
| * @param type |
| * The nsIUpdateItem type of the items to sort. |
| * @param propertyName |
| * The RDF property name used for sorting. |
| * @param isAscending |
| * true to sort ascending and false to sort descending |
| */ |
| sortTypeByProperty: function(type, propertyName, isAscending) { |
| var items = []; |
| var ctr = getContainer(this._inner, this._itemRoot); |
| var elements = ctr.GetElements(); |
| // Base 0 ordinal for checking against the existing order after sorting |
| var ordinal = 0; |
| while (elements.hasMoreElements()) { |
| var item = elements.getNext().QueryInterface(Ci.nsIRDFResource); |
| var id = stripPrefix(item.Value, PREFIX_ITEM_URI); |
| var itemType = this.getItemProperty(id, "type"); |
| if (itemType & type) { |
| items.push({ item : item, |
| ordinal: ordinal, |
| sortkey: this.getItemProperty(id, propertyName).toLowerCase() }); |
| ordinal++; |
| } |
| } |
| |
| var direction = isAscending ? 1 : -1; |
| // Case insensitive sort |
| function compare(a, b) { |
| if (a.sortkey < b.sortkey) return (-1 * direction); |
| if (a.sortkey > b.sortkey) return (1 * direction); |
| return 0; |
| } |
| items.sort(compare); |
| |
| // Check if there are any changes in the order of the items |
| var isDirty = false; |
| for (var i = 0; i < items.length; i++) { |
| if (items[i].ordinal != i) { |
| isDirty = true; |
| break; |
| } |
| } |
| |
| // If there are no changes then early return to avoid the perf impact |
| if (!isDirty) |
| return; |
| |
| // Reorder the items by moving them to the top of the container |
| this.beginUpdateBatch(); |
| for (i = 0; i < items.length; i++) { |
| ctr.RemoveElement(items[i].item, true); |
| ctr.InsertElementAt(items[i].item, i + 1, true); |
| } |
| this.endUpdateBatch(); |
| this.Flush(); |
| }, |
| |
| /** |
| * Determines if an Item is an active download |
| * @param id |
| * The ID of the item. This will be a uri scheme without the |
| * em item prefix so getProperty shouldn't be used. |
| * @returns true if the item is an active download, false otherwise. |
| */ |
| isDownloadItem: function(id) { |
| var downloadURL = stringData(this.GetTarget(gRDF.GetResource(id), EM_R("downloadURL"), true)); |
| return downloadURL && 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) { |
| // Updates have already been added to the datasource so we just update the |
| // download state. |
| if (addon.id != addon.xpiURL) { |
| this.updateDownloadState(PREFIX_ITEM_URI + addon.id, "waiting"); |
| 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.updateDownloadState(addon.xpiURL, "waiting"); |
| 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. |
| * @param type |
| * The nsIUpdateItem type of the item |
| * @param version |
| * The version of the item |
| */ |
| addIncompatibleUpdateItem: function(name, url, type, version) { |
| var iconURL = (type == Ci.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.updateDownloadState(url, "incompatibleUpdate"); |
| 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.updateDownloadState(url, null); |
| this.Flush(); |
| }, |
| |
| /** |
| * A hash of RDF resource values (e.g. Add-on IDs or XPI URLs) that represent |
| * installation progress for a single browser session. |
| */ |
| _progressData: { }, |
| |
| /** |
| * Updates the install progress data for a given ID (e.g. Add-on IDs or |
| * XPI URLs). |
| * @param id |
| * The URL string of the active download to be removed |
| * @param state |
| * The current state in the installation process. If null the object |
| * is deleted from _progressData. |
| */ |
| updateDownloadState: function(id, state) { |
| if (!state) { |
| if (id in this._progressData) |
| delete this._progressData[id]; |
| return; |
| } |
| else { |
| if (!(id in this._progressData)) |
| this._progressData[id] = { }; |
| this._progressData[id].state = state; |
| } |
| var item = gRDF.GetResource(id); |
| this._updateProperty(item, "state"); |
| }, |
| |
| updateDownloadProgress: function(id, progress) { |
| if (!progress) { |
| if (!(id in this._progressData)) |
| return; |
| this._progressData[id].progress = null; |
| } |
| else { |
| if (!(id in this._progressData)) |
| this.updateDownloadState(id, "downloading"); |
| |
| if (this._progressData[id].progress == progress) |
| return; |
| |
| this._progressData[id].progress = progress; |
| } |
| var item = gRDF.GetResource(id); |
| this._updateProperty(item, "progress"); |
| }, |
| |
| /** |
| * 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(Ci.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 || !oldLocation || 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]); |
| try { |
| this._inner = gRDF.GetDataSourceBlocking(getURLSpecFromFile(extensionsFile)); |
| } |
| catch (e) { |
| ERROR("Datasource::loadExtensions: removing corrupted extensions datasource " + |
| " file = " + extensionsFile.path + ", exception = " + e + "\n"); |
| extensionsFile.remove(false); |
| return; |
| } |
| |
| var cu = Cc["@mozilla.org/rdf/container-utils;1"]. |
| getService(Ci.nsIRDFContainerUtils); |
| cu.MakeSeq(this._inner, this._itemRoot); |
| |
| this._buildVisibleItemList(); |
| }, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| onUpdateStarted: function() { |
| LOG("Datasource: Update Started"); |
| }, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| onUpdateEnded: function() { |
| LOG("Datasource: Update Ended"); |
| }, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| onAddonUpdateStarted: function(addon) { |
| if (!addon) |
| throw Cr.NS_ERROR_INVALID_ARG; |
| |
| LOG("Datasource: Addon Update Started: " + addon.id); |
| this.updateProperty(addon.id, "availableUpdateURL"); |
| }, |
| |
| /** |
| * See nsIExtensionManager.idl |
| */ |
| onAddonUpdateEnded: function(addon, status) { |
| if (!addon) |
| throw Cr.NS_ERROR_INVALID_ARG; |
| |
| LOG("Datasource: Addon Update Ended: " + addon.id + ", status: " + status); |
| var url = null, hash = null, version = null; |
| var updateAvailable = status == Ci.nsIAddonUpdateCheckListener.STATUS_UPDATE; |
| if (updateAvailable) { |
| url = EM_L(addon.xpiURL); |
| if (addon.xpiHash) |
| hash = EM_L(addon.xpiHash); |
| version = EM_L(addon.version); |
| } |
| this.setItemProperties(addon.id, { |
| availableUpdateURL: url, |
| availableUpdateHash: hash, |
| availableUpdateVersion: version |
| }); |
| this.updateProperty(addon.id, "availableUpdateURL"); |
| }, |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // 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); |
| if (!installLocation) |
| return fallbackURL; |
| 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 & Ci.nsIUpdateItem.TYPE_THEME) |
| return this._getThemeImageURL(item, "icon.png", URI_GENERIC_ICON_THEME); |
| |
| if (inSafeMode()) |
| return gRDF.GetResource(URI_GENERIC_ICON_XPINSTALL); |
| |
| var hasIconURL = this._inner.hasArcOut(item, property); |
| // If the addon doesn't have an IconURL property or it is disabled use the |
| // generic icon URL instead. |
| if (!hasIconURL || this.getItemProperty(id, "isDisabled") == "true") |
| return gRDF.GetResource(URI_GENERIC_ICON_XPINSTALL); |
| var iconURL = stringData(this._inner.GetTarget(item, property, true)); |
| try { |
| var uri = newURI(iconURL); |
| var scheme = uri.scheme; |
| // Only allow chrome URIs or when installing http(s) URIs. |
| if (scheme == "chrome" || (scheme == "http" || scheme == "https") && |
| this._inner.hasArcOut(item, EM_R("downloadURL"))) |
| return null; |
| } |
| catch (e) { |
| } |
| // Use a generic icon URL for addons that have an invalid iconURL. |
| return gRDF.GetResource(URI_GENERIC_ICON_XPINSTALL); |
| }, |
| |
| /** |
| * 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 & Ci.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, "isDisabled") == "true" || |
| this.getItemProperty(id, "opType") == OP_NEEDS_UPGRADE) |
| return EM_L(""); |
| |
| return null; |
| }, |
| |
| _rdfGet_installDate: function(item, property) { |
| var id = stripPrefix(item.Value, PREFIX_ITEM_URI); |
| var key = this.getItemProperty(id, "installLocation"); |
| if (key && key in StartupCache.entries && id in StartupCache.entries[key] && |
| StartupCache.entries[key][id] && StartupCache.entries[key][id].mtime) |
| return EM_D(StartupCache.entries[key][id].mtime * 1000000); |
| 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) { |
| // When installing a new addon targetAppInfo does not exist yet |
| if (this.getItemProperty(id, "opType") == OP_NEEDS_INSTALL) |
| return EM_L("true"); |
| return EM_L("false"); |
| } |
| |
| getVersionChecker(); |
| var appVersion = targetAppInfo.appID == TOOLKIT_ID ? gApp.platformVersion : 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"); |
| }, |
| |
| /** |
| * Get the providesUpdatesSecurely property (whether or not this item has a |
| * secure update mechanism) |
| */ |
| _rdfGet_providesUpdatesSecurely: function(item, property) { |
| var id = stripPrefix(item.Value, PREFIX_ITEM_URI); |
| if (this.getItemProperty(id, "updateKey") || |
| !this.getItemProperty(id, "updateURL") || |
| this.getItemProperty(id, "updateURL").substring(0, 6) == "https:") |
| return EM_L("true"); |
| return EM_L("false"); |
| }, |
| |
| /** |
| * Get the em:blocklisted property (whether or not this item is blocklisted) |
| */ |
| _rdfGet_blocklisted: function(item, property) { |
| var id = stripPrefix(item.Value, PREFIX_ITEM_URI); |
| var version = this.getItemProperty(id, "version"); |
| if (!gBlocklist) |
| gBlocklist = Cc["@mozilla.org/extensions/blocklist;1"]. |
| getService(Ci.nsIBlocklistService); |
| if (gBlocklist.isAddonBlocklisted(id, version, null, null)) |
| return EM_L("true"); |
| |
| return EM_L("false"); |
| }, |
| |
| /** |
| * Get the em:state property (represents the current phase of an install). |
| */ |
| _rdfGet_state: function(item, property) { |
| var id = item.Value; |
| if (id in this._progressData) |
| return EM_L(this._progressData[id].state); |
| return null; |
| }, |
| |
| /** |
| * Get the em:progress property from the _progressData js object. By storing |
| * progress which is updated repeastedly during a download we avoid |
| * repeastedly writing it to the rdf file. |
| */ |
| _rdfGet_progress: function(item, property) { |
| var id = item.Value; |
| if (id in this._progressData) |
| return EM_I(this._progressData[id].progress); |
| return null; |
| }, |
| |
| /** |
| * Get the em:appManaged property. This prevents extensions from hiding |
| * extensions installed into locations other than the app-global location. |
| */ |
| _rdfGet_appManaged: function(item, property) { |
| var id = stripPrefix(item.Value, PREFIX_ITEM_URI); |
| var locationKey = this.getItemProperty(id, "installLocation"); |
| if (locationKey != KEY_APP_GLOBAL) |
| return EM_L("false"); |
| return null; |
| }, |
| |
| /** |
| * Get the em:hidden property. This prevents extensions from hiding |
| * extensions installed into locations other than restricted locations. |
| */ |
| _rdfGet_hidden: function(item, property) { |
| var id = stripPrefix(item.Value, PREFIX_ITEM_URI); |
| var installLocation = InstallLocations.get(this.getInstallLocationKey(id)); |
| if (!installLocation || !installLocation.restricted) |
| return EM_L("false"); |
| return null; |
| }, |
| |
| /** |
| * Get the em:locked property. This prevents extensions from locking |
| * extensions installed into locations other than restricted locations. |
| */ |
| _rdfGet_locked: function(item, property) { |
| var id = stripPrefix(item.Value, PREFIX_ITEM_URI); |
| var installLocation = InstallLocations.get(this.getInstallLocationKey(id)); |
| if (!installLocation || !installLocation.restricted) |
| return EM_L("false"); |
| return null; |
| }, |
| |
| /** |
| * Get the em:satisfiesDependencies property - literal string "false" for |
| * dependencies not satisfied (e.g. dependency disabled, incorrect version, |
| * not installed etc.), and literal string "true" for dependencies satisfied. |
| */ |
| _rdfGet_satisfiesDependencies: function(item, property) { |
| var id = stripPrefix(item.Value, PREFIX_ITEM_URI); |
| if (this.satisfiesDependencies(id)) |
| return EM_L("true"); |
| return EM_L("false"); |
| }, |
| |
| /** |
| * 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] && StartupCache.entries[key][id].op != OP_NONE) |
| return EM_L(StartupCache.entries[key][id].op); |
| return null; |
| }, |
| |
| /** |
| * 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, |
| Ci.nsIPrefLocalizedString); |
| if (value.data) |
| return EM_L(value.data); |
| } |
| catch (e) { |
| } |
| |
| var localized = findClosestLocalizedResource(this._inner, item); |
| if (localized) { |
| var value = this._inner.GetTarget(localized, property, true); |
| return value ? value : EM_L(""); |
| } |
| 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); |
| }, |
| |
| _rdfGet_availableUpdateInfo: function(item, property) { |
| var id = stripPrefix(item.Value, PREFIX_ITEM_URI); |
| var uri = stringData(this._inner.GetTarget(item, EM_R("availableUpdateInfo"), true)); |
| if (uri) { |
| uri = escapeAddonURI(this.getItemForID(id), uri, this); |
| return EM_L(uri); |
| } |
| return null; |
| }, |
| |
| /** |
| * Get the em:isDisabled property. This will be true if the item has a |
| * appDisabled or a userDisabled property that is true or OP_NEEDS_ENABLE. |
| */ |
| _rdfGet_isDisabled: 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, "userDisabled") == OP_NEEDS_ENABLE || |
| this.getItemProperty(id, "appDisabled") == OP_NEEDS_ENABLE) |
| return EM_L("true"); |
| return EM_L("false"); |
| }, |
| |
| _rdfGet_addonID: function(item, property) { |
| var id = this._inner.GetTarget(item, EM_R("downloadURL"), true) ? item.Value : |
| stripPrefix(item.Value, PREFIX_ITEM_URI); |
| return EM_L(id); |
| }, |
| |
| /** |
| * 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_NONE || this.getItemProperty(id, "appManaged") == "true") |
| return EM_L("false"); |
| |
| if (getPref("getBoolPref", (PREF_EM_ITEM_UPDATE_ENABLED.replace(/%UUID%/, id), false)) == true) |
| return EM_L("false"); |
| |
| 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; |
| while (true) { |
| try { |
| var value = gPref.getComplexValue(prefName + "." + ++i, |
| Ci.nsIPrefLocalizedString); |
| if (value.data) |
| values.push(EM_L(value.data)); |
| } |
| catch (e) { |
| try { |
| var value = gPref.getComplexValue(prefName, |
| Ci.nsIPrefLocalizedString); |
| if (value.data) |
| values.push(EM_L(value.data)); |
| } |
| catch (e) { |
| } |
| break; |
| } |
| } |
| if (values.length > 0) |
| return values; |
| |
| var localized = findClosestLocalizedResource(this._inner, item); |
| if (localized) { |
| var targets = this._inner.GetTargets(localized, property, true); |
| while (targets.hasMoreElements()) |
| values.push(targets.getNext()); |
| return values; |
| } |
| return null; |
| }, |
| |
| /** |
| * Get the em:developer property (developers of the extension) |
| */ |
| _rdfGets_developer: function(item, property) { |
| return this._getLocalizablePropertyValues(item, property); |
| }, |
| |
| /** |
| * Get the em:translator property (translators of the extension) |
| */ |
| _rdfGets_translator: 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 propertyName = stripPrefix(property.Value, PREFIX_NS_EM); |
| var getter = "_rdfGets_" + propertyName; |
| if (getter in this) |
| ary = this[getter](source, property); |
| else { |
| // The template builder calls GetTargets when single value properties |
| // are used in a triple. |
| getter = "_rdfGet_" + propertyName; |
| 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; |
| |
| var getter = "_rdfGet_" + stripPrefix(property.Value, PREFIX_NS_EM); |
| if (getter in this) |
| return this[getter](source, property) == target; |
| 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 Cr.NS_ERROR_NOT_IMPLEMENTED; |
| }, |
| |
| Init: function(uri) { |
| }, |
| |
| Refresh: function(blocking) { |
| }, |
| |
| Flush: function() { |
| // For some operations we block repeated flushing until all operations |
| // are complete to reduce file accesses that can trigger bug 431065 |
| if (!gAllowFlush) { |
| gDSNeedsFlush = true; |
| return; |
| } |
| if (this._inner instanceof Ci.nsIRDFRemoteDataSource) |
| this._inner.Flush(); |
| }, |
| |
| FlushTo: function(uri) { |
| }, |
| |
| QueryInterface: XPCOMUtils.generateQI([Ci.nsIRDFDataSource, |
| Ci.nsIRDFRemoteDataSource]) |
| }; |
| |
| function UpdateItem () {} |
| UpdateItem.prototype = { |
| /** |
| * See nsIUpdateService.idl |
| */ |
| init: function(id, version, installLocationKey, minAppVersion, maxAppVersion, |
| name, downloadURL, xpiHash, iconURL, updateURL, updateKey, type, |
| targetAppID) { |
| 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._updateKey = updateKey; |
| this._type = type; |
| this._targetAppID = targetAppID; |
| }, |
| |
| /** |
| * 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 updateKey() { return this._updateKey; }, |
| get type() { return this._type; }, |
| get targetAppID() { return this._targetAppID; }, |
| |
| /** |
| * 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, |
| updateKey : this._updateKey, |
| type : this._type, |
| targetAppID : this._targetAppID |
| }.toSource(); |
| }, |
| |
| classDescription: "Update Item", |
| contractID: "@mozilla.org/updates/item;1", |
| classID: Components.ID("{F3294B1C-89F4-46F8-98A0-44E1EAE92518}"), |
| QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateItem]) |
| }; |
| |
| var gEmSingleton = null; |
| var EmFactory = { |
| createInstance: function(outer, iid) { |
| if (outer != null) |
| throw Cr.NS_ERROR_NO_AGGREGATION; |
| |
| if (!gEmSingleton) |
| gEmSingleton = new ExtensionManager(); |
| return gEmSingleton.QueryInterface(iid); |
| } |
| }; |
| |
| function DatasourceModule() {} |
| DatasourceModule.prototype = { |
| classDescription: "Extension Manager Data Source", |
| contractID: "@mozilla.org/rdf/datasource;1?name=extensions", |
| classID: Components.ID("{69BB8313-2D4F-45EC-97E0-D39DA58ECCE9}"), |
| _xpcom_factory: { |
| createInstance: function() Cc[ExtensionManager.prototype.contractID]. |
| getService(Ci.nsIExtensionManager).datasource |
| } |
| }; |
| |
| |
| function NSGetModule(compMgr, fileSpec) |
| XPCOMUtils.generateModule([ExtensionManager, DatasourceModule, UpdateItem]); |
| |