--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -408,16 +408,43 @@ function setFilePermissions(aFile, aPerm
}
catch (e) {
logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " +
aFile.path, e);
}
}
/**
+ * Write a given string to a file
+ *
+ * @param file
+ * The nsIFile instance to write into
+ * @param string
+ * The string to write
+ */
+function writeStringToFile(file, string) {
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(Ci.nsIConverterOutputStream);
+
+ try {
+ stream.init(file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
+ 0);
+ converter.init(stream, "UTF-8", 0, 0x0000);
+ converter.writeString(string);
+ }
+ finally {
+ converter.close();
+ stream.close();
+ }
+}
+
+/**
* A safe way to install a file or the contents of a directory to a new
* directory. The file or directory is moved or copied recursively and if
* anything fails an attempt is made to rollback the entire operation. The
* operation may also be rolled back to its original state after it has
* completed by calling the rollback method.
*
* Operations can be chained. Calling move or copy multiple times will remember
* the whole set and if one fails all of the operations will be rolled back.
@@ -3524,18 +3551,21 @@ this.XPIProvider = {
flushChromeCaches();
}
}
catch (e) {
}
}
try {
- addon._sourceBundle = location.installAddon(id, stageDirEntry,
- existingAddonID);
+ addon._sourceBundle = location.installAddon({
+ id,
+ source: stageDirEntry,
+ existingAddonID
+ });
}
catch (e) {
logger.error("Failed to install staged add-on " + id + " in " + location.name,
e);
// Re-create the staged install
AddonInstall.createStagedInstall(location, stageDirEntry,
addon);
// Make sure not to delete the cached manifest json file
@@ -3666,17 +3696,17 @@ this.XPIProvider = {
}
}
else if (Preferences.get(PREF_BRANCH_INSTALLED_ADDON + id, false)) {
continue;
}
// Install the add-on
try {
- addon._sourceBundle = profileLocation.installAddon(id, entry, null, true);
+ addon._sourceBundle = profileLocation.installAddon({ id, source: entry, action: "copy" });
logger.debug("Installed distribution add-on " + id);
Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true)
// aManifests may contain a copy of a newly installed add-on's manifest
// and we'll have overwritten that so instead cache our install manifest
// which will later be put into the database in processFileChanges
if (!(KEY_APP_PROFILE in aManifests))
@@ -4022,25 +4052,58 @@ this.XPIProvider = {
/**
* Temporarily installs add-on from a local XPI file or directory.
* As this is intended for development, the signature is not checked and
* the add-on does not persist on application restart.
*
* @param aFile
* An nsIFile for the unpacked add-on directory or XPI file.
*
+ * @return See installAddonFromLocation return value.
+ */
+ installTemporaryAddon: function(aFile) {
+ return this.installAddonFromLocation(aFile, TemporaryInstallLocation);
+ },
+
+ /**
+ * Permanently installs add-on from a local XPI file or directory.
+ * The signature is checked but the add-on persist on application restart.
+ *
+ * @param aFile
+ * An nsIFile for the unpacked add-on directory or XPI file.
+ *
+ * @return See installAddonFromLocation return value.
+ */
+ installAddonFromSources: Task.async(function*(aFile) {
+ let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+ return this.installAddonFromLocation(aFile, location, "proxy");
+ }),
+
+ /**
+ * Installs add-on from a local XPI file or directory.
+ *
+ * @param aFile
+ * An nsIFile for the unpacked add-on directory or XPI file.
+ * @param aInstallLocation
+ * Define a custom install location object to use for the install.
+ * @param aInstallAction
+ * Optional action mode to use when installing the addon
+ * (see MutableDirectoryInstallLocation.installAddon)
+ *
* @return a Promise that resolves to an Addon object on success, or rejects
* if the add-on is not a valid restartless add-on or if the
- * same ID is already temporarily installed
- */
- installTemporaryAddon: Task.async(function*(aFile) {
+ * same ID is already installed.
+ */
+ installAddonFromLocation: Task.async(function*(aFile, aInstallLocation, aInstallAction) {
if (aFile.exists() && aFile.isFile()) {
flushJarCache(aFile);
}
- let addon = yield loadManifestFromFile(aFile, TemporaryInstallLocation);
+ let addon = yield loadManifestFromFile(aFile, aInstallLocation);
+
+ aInstallLocation.installAddon({ id: addon.id, source: aFile, action: aInstallAction });
if (addon.appDisabled) {
let message = `Add-on ${addon.id} is not compatible with application version.`;
let app = addon.matchingTargetApplication;
if (app) {
if (app.minVersion) {
message += ` add-on minVersion: ${app.minVersion}.`;
@@ -4049,17 +4112,17 @@ this.XPIProvider = {
message += ` add-on maxVersion: ${app.maxVersion}.`;
}
}
throw new Error(message);
}
if (!addon.bootstrap) {
throw new Error("Only restartless (bootstrap) add-ons"
- + " can be temporarily installed:", addon.id);
+ + " can be installed from sources:", addon.id);
}
let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL;
let oldAddon = yield new Promise(
resolve => XPIDatabase.getVisibleAddonForID(addon.id, resolve));
if (oldAddon) {
if (!oldAddon.bootstrap) {
logger.warn("Non-restartless Add-on is already installed", addon.id);
throw new Error("Non-restartless add-on with ID "
@@ -4104,16 +4167,17 @@ this.XPIProvider = {
addon.visible = true;
addon.enabled = true;
addon.active = true;
addon = XPIDatabase.addAddonMetadata(addon, file.persistentDescriptor);
XPIStates.addAddon(addon);
XPIDatabase.saveChanges();
+ XPIStates.save();
AddonManagerPrivate.callAddonListeners("onInstalling", addon.wrapper,
false);
XPIProvider.callBootstrapMethod(addon, file, "startup",
BOOTSTRAP_REASONS.ADDON_ENABLE);
AddonManagerPrivate.callInstallListeners("onExternalInstall",
null, addon.wrapper,
oldAddon ? oldAddon.wrapper : null,
@@ -6402,18 +6466,21 @@ AddonInstall.prototype = {
if (!isUpgrade && this.existingAddon.active) {
XPIDatabase.updateAddonActive(this.existingAddon, false);
}
}
// Install the new add-on into its final location
let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
- let file = this.installLocation.installAddon(this.addon.id, stagedAddon,
- existingAddonID);
+ let file = this.installLocation.installAddon({
+ id: this.addon.id,
+ source: stagedAddon,
+ existingAddonID
+ });
// Update the metadata in the database
this.addon._sourceBundle = file;
this.addon.visible = true;
if (isUpgrade) {
this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
file.persistentDescriptor);
@@ -6509,32 +6576,17 @@ AddonInstall.prototype = {
yield OS.File.copy(this.file.path, stagedAddon.path);
}
if (restartRequired) {
// Point the add-on to its extracted files as the xpi may get deleted
this.addon._sourceBundle = stagedAddon;
// Cache the AddonInternal as it may have updated compatibility info
- let stream = Cc["@mozilla.org/network/file-output-stream;1"].
- createInstance(Ci.nsIFileOutputStream);
- let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
- createInstance(Ci.nsIConverterOutputStream);
-
- try {
- stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
- FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
- 0);
- converter.init(stream, "UTF-8", 0, 0x0000);
- converter.writeString(JSON.stringify(this.addon));
- }
- finally {
- converter.close();
- stream.close();
- }
+ writeStringToFile(stagedJSON, JSON.stringify(this.addon));
logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart.");
if (isUpgrade) {
delete this.existingAddon.pendingUpgrade;
this.existingAddon.pendingUpgrade = this.addon;
}
}
@@ -8316,28 +8368,34 @@ Object.assign(MutableDirectoryInstallLoc
trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
return trashDir;
},
/**
* Installs an add-on into the install location.
*
- * @param aId
+ * @param id
* The ID of the add-on to install
- * @param aSource
+ * @param source
* The source nsIFile to install from
- * @param aExistingAddonID
+ * @param existingAddonID
* The ID of an existing add-on to uninstall at the same time
- * @param aCopy
- * If false the source files will be moved to the new location,
- * otherwise they will only be copied
+ * @param action
+ * What to we do with the given source file:
+ * "move"
+ * Default action, the source files will be moved to the new
+ * location,
+ * "copy"
+ * The source files will be copied,
+ * "proxy"
+ * A "proxy file" is going to refer to the source file path
* @return an nsIFile indicating where the add-on was installed to
*/
- installAddon: function(aId, aSource, aExistingAddonID, aCopy) {
+ installAddon: function({ id, source, existingAddonID, action = "move" }) {
let trashDir = this.getTrashDir();
let transaction = new SafeInstallOperation();
let moveOldAddon = aId => {
let file = this._directory.clone();
file.append(aId);
@@ -8350,79 +8408,90 @@ Object.assign(MutableDirectoryInstallLoc
flushJarCache(file);
transaction.moveUnder(file, trashDir);
}
}
// If any of these operations fails the finally block will clean up the
// temporary directory
try {
- moveOldAddon(aId);
- if (aExistingAddonID && aExistingAddonID != aId) {
- moveOldAddon(aExistingAddonID);
+ moveOldAddon(id);
+ if (existingAddonID && existingAddonID != id) {
+ moveOldAddon(existingAddonID);
{
// Move the data directories.
/* XXX ajvincent We can't use OS.File: installAddon isn't compatible
* with Promises, nor is SafeInstallOperation. Bug 945540 has been filed
* for porting to OS.File.
*/
let oldDataDir = FileUtils.getDir(
- KEY_PROFILEDIR, ["extension-data", aExistingAddonID], false, true
+ KEY_PROFILEDIR, ["extension-data", existingAddonID], false, true
);
if (oldDataDir.exists()) {
let newDataDir = FileUtils.getDir(
- KEY_PROFILEDIR, ["extension-data", aId], false, true
+ KEY_PROFILEDIR, ["extension-data", id], false, true
);
if (newDataDir.exists()) {
let trashData = trashDir.clone();
trashData.append("data-directory");
transaction.moveUnder(newDataDir, trashData);
}
transaction.moveTo(oldDataDir, newDataDir);
}
}
}
- if (aCopy) {
- transaction.copy(aSource, this._directory);
- }
- else {
- if (aSource.isFile())
- flushJarCache(aSource);
-
- transaction.moveUnder(aSource, this._directory);
- }
+ if (action == "copy") {
+ transaction.copy(source, this._directory);
+ }
+ else if (action == "move") {
+ if (source.isFile())
+ flushJarCache(source);
+
+ transaction.moveUnder(source, this._directory);
+ }
+ // Do nothing for the proxy file as we sideload an addon permanently
}
finally {
// It isn't ideal if this cleanup fails but it isn't worth rolling back
// the install because of it.
try {
recursiveRemove(trashDir);
}
catch (e) {
- logger.warn("Failed to remove trash directory when installing " + aId, e);
+ logger.warn("Failed to remove trash directory when installing " + id, e);
}
}
let newFile = this._directory.clone();
- newFile.append(aSource.leafName);
+
+ if (action == "proxy") {
+ // When permanently installing sideloaded addon, we just put a proxy file
+ // refering to the addon sources
+ newFile.append(id);
+
+ writeStringToFile(newFile, source.path);
+ } else {
+ newFile.append(source.leafName);
+ }
+
try {
newFile.lastModifiedTime = Date.now();
} catch (e) {
logger.warn("failed to set lastModifiedTime on " + newFile.path, e);
}
- this._IDToFileMap[aId] = newFile;
- XPIProvider._addURIMapping(aId, newFile);
-
- if (aExistingAddonID && aExistingAddonID != aId &&
- aExistingAddonID in this._IDToFileMap) {
- delete this._IDToFileMap[aExistingAddonID];
+ this._IDToFileMap[id] = newFile;
+ XPIProvider._addURIMapping(id, newFile);
+
+ if (existingAddonID && existingAddonID != id &&
+ existingAddonID in this._IDToFileMap) {
+ delete this._IDToFileMap[existingAddonID];
}
return newFile;
},
/**
* Uninstalls an add-on from this location.
*
@@ -8819,19 +8888,17 @@ WinRegInstallLocation.prototype = {
* @param key
* The key that contains the ID to path mapping
*/
_readAddons: function(aKey) {
let count = aKey.valueCount;
for (let i = 0; i < count; ++i) {
let id = aKey.getValueName(i);
- let file = Cc["@mozilla.org/file/local;1"].
- createInstance(Ci.nsIFile);
- file.initWithPath(aKey.readStringValue(id));
+ let file = new nsIFile(aKey.readStringValue(id));
if (!file.exists()) {
logger.warn("Ignoring missing add-on in " + file.path);
continue;
}
this._IDToFileMap[id] = file;
XPIProvider._addURIMapping(id, file);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_from_sources.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const ID = "bootstrap1@tests.mozilla.org";
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+startupManager();
+
+BootstrapMonitor.init();
+
+// Partial list of bootstrap reasons from XPIProvider.jsm
+const BOOTSTRAP_REASONS = {
+ ADDON_INSTALL: 5,
+ ADDON_UPGRADE: 7,
+ ADDON_DOWNGRADE: 8,
+};
+
+// Install an unsigned add-on with no existing add-on present.
+// Restart and make sure it is still around.
+add_task(function*() {
+ let extInstallCalled = false;
+ AddonManager.addInstallListener({
+ onExternalInstall: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ do_check_eq(aInstall.version, "1.0");
+ extInstallCalled = true;
+ },
+ });
+
+ let installingCalled = false;
+ let installedCalled = false;
+ AddonManager.addAddonListener({
+ onInstalling: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ do_check_eq(aInstall.version, "1.0");
+ installingCalled = true;
+ },
+ onInstalled: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ do_check_eq(aInstall.version, "1.0");
+ installedCalled = true;
+ },
+ onInstallStarted: (aInstall) => {
+ do_throw("onInstallStarted called unexpectedly");
+ }
+ });
+
+ yield AddonManager.installAddonFromSources(do_get_file("data/from_sources/"));
+
+ do_check_true(extInstallCalled);
+ do_check_true(installingCalled);
+ do_check_true(installedCalled);
+
+ let install = BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ equal(install.reason, BOOTSTRAP_REASONS.ADDON_INSTALL);
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ let addon = yield promiseAddonByID(ID);
+
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Bootstrap 1");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ yield promiseRestartManager();
+
+ install = BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ equal(install.reason, BOOTSTRAP_REASONS.ADDON_INSTALL);
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ yield promiseRestartManager();
+});
+