Bug 356370: "installLocation has no properties" during install/update of extensions. r=robstrong

This commit is contained in:
dtownsend@oxymoronical.com 2008-04-01 02:26:47 -07:00
parent 4f437d0ce7
commit 101a8d753a
6 changed files with 340 additions and 88 deletions

View File

@ -2344,6 +2344,9 @@ var StartupCache = {
// 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)
@ -2884,6 +2887,81 @@ ExtensionManager.prototype = {
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
@ -2891,80 +2969,6 @@ ExtensionManager.prototype = {
*/
_checkForFileChanges: function() {
var em = this;
/**
* Configure an item that was installed or upgraded by another process
* so that |_finishOperations| can properly complete processing and
* registration.
* As this is the only point at which we can reliably know the Install
* Location of this item, we use this as an opportunity to:
* 1. Check that this item is compatible with this Firefox version.
* 2. If it is, configure the item by using the supplied callback.
* We do not do any special handling in the case that the item is
* not compatible with this version other than to simply not register
* it and log that fact - there is no "phone home" check for updates.
* It may or may not make sense to do this, but for now we'll just
* not register.
* @param id
* The GUID of the item to validate and configure.
* @param location
* The Install Location where this item is installed.
* @param callback
* The callback that configures the item for installation upon
* successful validation.
*/
function installItem(id, location, callback) {
// As this is the only pint at which we reliably know the installation
var installRDF = location.getItemFile(id, FILE_INSTALL_MANIFEST);
if (installRDF.exists()) {
LOG("Item Installed/Upgraded at Install Location: " + location.name +
" Item ID: " + id + ", attempting to register...");
var installManifest = getInstallManifest(installRDF);
var installData = em._getInstallData(installManifest);
if (installData.error == INSTALLERROR_SUCCESS) {
LOG("... success, item is compatible");
callback(installManifest, installData.id, location, installData.type);
}
else if (installData.error == INSTALLERROR_INCOMPATIBLE_VERSION) {
LOG("... success, item installed but is not compatible");
callback(installManifest, installData.id, location, installData.type);
em._appDisableItem(id);
}
else if (installData.error == INSTALLERROR_INSECURE_UPDATE) {
LOG("... success, item installed but does not provide updates securely");
callback(installManifest, installData.id, location, installData.type);
em._appDisableItem(id);
}
else if (installData.error == INSTALLERROR_BLOCKLISTED) {
LOG("... success, item installed but is blocklisted");
callback(installManifest, installData.id, location, installData.type);
em._appDisableItem(id);
}
else {
/**
* Turns an error code into a message for logging
* @param error
* an Install Error code
* @returns A string message to be logged.
*/
function translateErrorMessage(error) {
switch (error) {
case INSTALLERROR_INVALID_GUID:
return "Invalid GUID";
case INSTALLERROR_INVALID_VERSION:
return "Invalid Version";
case INSTALLERROR_INCOMPATIBLE_PLATFORM:
return "Incompatible Platform";
}
}
LOG("... failure, item is not compatible, error: " +
translateErrorMessage(installData.error));
// Add the item to the Startup Cache anyway, so we don't re-detect it
// every time the app starts.
StartupCache.put(location, id, OP_NONE, true);
}
}
}
/**
* Determines if an item can be used based on whether or not the install
@ -3181,11 +3185,11 @@ ExtensionManager.prototype = {
LOG("Item Location path changed: " + lf.path + " Item ID: " +
id + " Location Key: " + location.name + ", attempting to upgrade item...");
if (canUse(id, location)) {
installItem(id, location,
function(installManifest, id, location, type) {
em._upgradeItem(installManifest, id, location,
type);
});
this.installItem(id, location,
function(installManifest, id, location, type) {
em._upgradeItem(installManifest, id, location,
type);
});
isDirty = true;
}
}
@ -3232,11 +3236,11 @@ ExtensionManager.prototype = {
if (canUse(id, location)) {
LOG("Item Installed via directory addition to Install Location: " +
location.name + " Item ID: " + id + ", attempting to register...");
installItem(id, location,
function(installManifest, id, location, type) {
em._configureForthcomingItem(installManifest, id, location,
type);
});
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");
@ -3596,17 +3600,24 @@ ExtensionManager.prototype = {
}
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.
var location = this.getInstallLocation(id);
if (location.name == KEY_APP_GLOBAL) {
var installRDF = location.getItemFile(id, FILE_INSTALL_MANIFEST);
if (installRDF.exists()) {
@ -3622,7 +3633,6 @@ ExtensionManager.prototype = {
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. */
location = this.getInstallLocation(id);
installRDF = location.getItemFile(id, FILE_INSTALL_MANIFEST);
if (installRDF.exists()) {
metadataDS = getInstallManifest(installRDF);
@ -3644,6 +3654,36 @@ ExtensionManager.prototype = {
ds.setItemProperty(id, EM_R("availableUpdateURL"), null);
ds.setItemProperty(id, EM_R("availableUpdateVersion"), null);
}
// 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);
@ -3894,9 +3934,12 @@ ExtensionManager.prototype = {
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.
var installLocation = this.getInstallLocation(item.id);
if (installLocation.name in StartupCache.entries &&
item.id in StartupCache.entries[installLocation.name] &&
StartupCache.entries[installLocation.name][item.id]) {
@ -4780,7 +4823,7 @@ ExtensionManager.prototype = {
var ds = this.datasource;
var type = ds.getItemProperty(id, "type");
if (id == 0 || id == -1) {
ds.removeCorruptItem(id, type);
ds.removeCorruptItem(id);
return;
}
var installLocation = this.getInstallLocation(id);
@ -7539,6 +7582,7 @@ ExtensionsDataSource.prototype = {
removeCorruptItem: function(id) {
this.removeItemMetadata(id);
this.removeItemFromContainer(id);
this.visibleItems[id] = null;
},
/**

View File

@ -0,0 +1,50 @@
<?xml version="1.0"?>
<RDF:RDF xmlns:em="http://www.mozilla.org/2004/em-rdf#"
xmlns:NC="http://home.netscape.com/NC-rdf#"
xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<RDF:Description RDF:about="urn:mozilla:item:bug356370_1@tests.mozilla.org">
<em:installLocation>app-profile</em:installLocation>
<em:version>1</em:version>
<em:name>Bug 356370 test 1</em:name>
<em:type NC:parseType="Integer">2</em:type>
<em:targetApplication>
<RDF:Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>1</em:maxVersion>
</RDF:Description>
</em:targetApplication>
</RDF:Description>
<RDF:Description RDF:about="urn:mozilla:item:bug356370_2@tests.mozilla.org">
<em:installLocation>invalid-hi</em:installLocation>
<em:version>1</em:version>
<em:name>Bug 356370 test 2</em:name>
<em:type NC:parseType="Integer">2</em:type>
<em:userDisabled>true</em:userDisabled>
<em:targetApplication>
<RDF:Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>1</em:maxVersion>
</RDF:Description>
</em:targetApplication>
</RDF:Description>
<RDF:Description RDF:about="urn:mozilla:item:bug356370_3@tests.mozilla.org">
<em:installLocation>invalid</em:installLocation>
<em:version>1</em:version>
<em:name>Bug 356370 test 3</em:name>
<em:type NC:parseType="Integer">2</em:type>
<em:targetApplication>
<RDF:Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>1</em:maxVersion>
</RDF:Description>
</em:targetApplication>
</RDF:Description>
<RDF:Seq RDF:about="urn:mozilla:item:root">
<RDF:li RDF:resource="urn:mozilla:item:bug356370_1@tests.mozilla.org"/>
<RDF:li RDF:resource="urn:mozilla:item:bug356370_2@tests.mozilla.org"/>
<RDF:li RDF:resource="urn:mozilla:item:bug356370_3@tests.mozilla.org"/>
</RDF:Seq>
</RDF:RDF>

View File

@ -0,0 +1,20 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>bug356370_1@tests.mozilla.org</em:id>
<em:version>1</em:version>
<em:targetApplication>
<Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>1</em:maxVersion>
</Description>
</em:targetApplication>
<em:name>Bug 356370 test 1</em:name>
</Description>
</RDF>

View File

@ -0,0 +1,20 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>bug356370_2@tests.mozilla.org</em:id>
<em:version>1</em:version>
<em:targetApplication>
<Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>1</em:maxVersion>
</Description>
</em:targetApplication>
<em:name>Bug 356370 test 2</em:name>
</Description>
</RDF>

View File

@ -227,6 +227,7 @@ function startupEM()
needsRestart = gEM.checkForMismatches();
}
catch (e) {
dump("checkForMismatches threw an exception: " + e + "\n");
needsRestart = false;
upgraded = false;
}

View File

@ -0,0 +1,117 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is mozilla.org code.
*
* The Initial Developer of the Original Code is
* Dave Townsend <dtownsend@oxymoronical.com>.
*
* Portions created by the Initial Developer are Copyright (C) 2008
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK *****
*/
function write_cache_line(stream, location, id, mtime) {
var line = location + "\t" + id + "\trel%" + id + "\t" + Math.floor(mtime / 1000) + "\t\r\n";
stream.write(line, line.length);
}
/**
* This copies two extensions, a default extensions datasource into the profile
* It also manufactures an extensions.cache file with invalid items.
* There are 3 test extensions:
* bug356370_1@tests.mozilla.org exists in app-profile and an unused version is in invalid-lo
* bug356370_2@tests.mozilla.org exists in invalid-hi and an unusd version is in app-profile
* bug356370_3@tests.mozilla.org exists in invalid
*
* After startup only the first two should exist in the correct install location
* and installing extensions should be successful.
*/
function setup_profile() {
// Set up the profile with some existing extensions
// Not nice to copy the extensions datasource in, but bringing up the EM to
// create it properly will invalidate the test
var source = do_get_file("toolkit/mozapps/extensions/test/unit/data/test_bug356370.rdf");
source.copyTo(gProfD, "extensions.rdf");
// Must programmatically generate the cache since it depends on the mimetimes
// being accurate.
var cache = gProfD.clone();
cache.append("extensions.cache");
var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
.createInstance(Components.interfaces.nsIFileOutputStream);
foStream.init(cache, 0x02 | 0x08 | 0x20, 0666, 0); // Write, create, truncate
var addon = gProfD.clone();
addon.append("extensions");
addon.append("bug356370_1@tests.mozilla.org");
source = do_get_file("toolkit/mozapps/extensions/test/unit/data/test_bug356370_1.rdf");
addon.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
source.copyTo(addon, "install.rdf");
write_cache_line(foStream, "app-profile", "bug356370_1@tests.mozilla.org",
addon.lastModifiedTime);
addon = gProfD.clone();
addon.append("extensions");
addon.append("bug356370_2@tests.mozilla.org");
source = do_get_file("toolkit/mozapps/extensions/test/unit/data/test_bug356370_2.rdf");
addon.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
source.copyTo(addon, "install.rdf");
write_cache_line(foStream, "app-profile", "bug356370_2@tests.mozilla.org",
addon.lastModifiedTime);
// Write out a set of invalid entries
write_cache_line(foStream, "invalid-lo", "bug356370_1@tests.mozilla.org", 0);
write_cache_line(foStream, "invalid-hi", "bug356370_2@tests.mozilla.org", 0);
write_cache_line(foStream, "invalid", "bug356370_3@tests.mozilla.org", 0);
foStream.close();
}
function run_test() {
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
gPrefs.setCharPref("extensions.lastAppVersion", "4");
setup_profile();
startupEM();
do_check_neq(gEM.getItemForID("bug356370_1@tests.mozilla.org"), null);
do_check_eq(getManifestProperty("bug356370_1@tests.mozilla.org", "installLocation"), "app-profile");
do_check_neq(gEM.getItemForID("bug356370_2@tests.mozilla.org"), null);
do_check_eq(getManifestProperty("bug356370_2@tests.mozilla.org", "installLocation"), "app-profile");
// This should still be disabled
do_check_eq(getManifestProperty("bug356370_2@tests.mozilla.org", "isDisabled"), "true");
do_check_eq(gEM.getItemForID("bug356370_3@tests.mozilla.org"), null);
gEM.installItemFromFile(do_get_addon("test_bug257155"), NS_INSTALL_LOCATION_APPPROFILE);
do_check_neq(gEM.getItemForID("bug257155@tests.mozilla.org"), null);
restartEM();
do_check_neq(gEM.getItemForID("bug257155@tests.mozilla.org"), null);
do_check_neq(gEM.getItemForID("bug356370_1@tests.mozilla.org"), null);
do_check_neq(gEM.getItemForID("bug356370_2@tests.mozilla.org"), null);
do_check_eq(gEM.getItemForID("bug356370_3@tests.mozilla.org"), null);
}