Bug 558834: Add support for targetPlatform, size and skinnable properties to the database. r=robstrong

This commit is contained in:
Dave Townsend 2010-07-01 11:35:48 -07:00
parent a091601fc2
commit 33fdcc33d0
2 changed files with 276 additions and 78 deletions

View File

@ -100,15 +100,23 @@ const TOOLKIT_ID = "toolkit@mozilla.org";
const BRANCH_REGEXP = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
const DB_SCHEMA = 1;
const DB_SCHEMA = 2;
const REQ_VERSION = 2;
// Properties that exist in the install manifest
const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL",
"updateKey", "optionsURL", "aboutURL", "iconURL"]
const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"];
const PROP_TARGETAPP = ["id", "minVersion", "maxVersion"];
// Properties that only exist in the database
const DB_METADATA = ["installDate", "updateDate", "size", "sourceURI",
"releaseNotesURI"];
const DB_BOOL_METADATA = ["visible", "active", "userDisabled", "appDisabled",
"pendingUninstall", "applyBackgroundUpdates",
"bootstrap", "skinnable"];
const BOOTSTRAP_REASONS = {
APP_STARTUP : 1,
APP_SHUTDOWN : 2,
@ -404,21 +412,26 @@ function loadManifestFromRDF(aUri, aStream) {
}
}
}
if (!(addon.type in TYPES))
throw new Error("Install manifest specifies unknown type: " + addon.type);
if (addon.type == "theme" && !addon.internalName)
throw new Error("Themes must include an internalName property");
// Only extensions are allowed to provide an optionsURL or aboutURL. For all
// other types they are silently ignored
if (addon.type != "extension") {
addon.optionsURL = null;
addon.aboutURL = null;
}
// Only read the bootstrapped property for extensions
if (addon.type == "extension")
if (addon.type == "extension") {
addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true";
}
else {
// Only extensions are allowed to provide an optionsURL or aboutURL. For
// all other types they are silently ignored
addon.optionsURL = null;
addon.aboutURL = null;
if (addon.type == "theme") {
if (!addon.internalName)
throw new Error("Themes must include an internalName property");
addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true";
}
}
addon.defaultLocale = readLocale(ds, root, true);
@ -455,13 +468,34 @@ function loadManifestFromRDF(aUri, aStream) {
addon.targetApplications.push(targetAppInfo);
}
addon.targetPlatforms = getPropertyArray(ds, root, "targetPlatform");
// Note that we don't need to check for duplicate targetPlatform entries since
// the RDF service coalesces them for us.
let targetPlatforms = getPropertyArray(ds, root, "targetPlatform");
addon.targetPlatforms = [];
targetPlatforms.forEach(function(aPlatform) {
let platform = {
os: null,
abi: null
};
let pos = aPlatform.indexOf("_");
if (pos != -1) {
platform.os = aPlatform.substring(0, pos);
platform.abi = aPlatform.substring(pos + 1);
}
else {
platform.os = aPlatform;
}
addon.targetPlatforms.push(platform);
});
// Themes are disabled by default unless they are currently selected
if (addon.type == "theme")
addon.userDisabled = addon.internalName != XPIProvider.selectedSkin;
else
addon.userDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
addon.appDisabled = !isUsableAddon(addon);
addon.applyBackgroundUpdates = true;
@ -478,6 +512,22 @@ function loadManifestFromRDF(aUri, aStream) {
* @throws if the directory does not contain a valid install manifest
*/
function loadManifestFromDir(aDir) {
function getFileSize(aFile) {
if (aFile.isSymlink())
return 0;
if (!aFile.isDirectory())
return aFile.fileSize;
let size = 0;
let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
let entry;
while (entry = entries.nextFile)
size += getFileSize(entry);
entries.close();
return size;
}
let file = aDir.clone();
file.append(FILE_INSTALL_MANIFEST);
if (!file.exists() || !file.isFile())
@ -494,6 +544,7 @@ function loadManifestFromDir(aDir) {
try {
let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis);
addon._sourceBundle = aDir.clone().QueryInterface(Ci.nsILocalFile);
addon.size = getFileSize(aDir);
return addon;
}
finally {
@ -502,6 +553,38 @@ function loadManifestFromDir(aDir) {
}
}
/**
* Loads an AddonInternal object from an add-on in a zip file.
*
* @param aZipReader
* An nsIZipReader open reading from the add-on XPI file
* @return an AddonInternal object
* @throws if the directory does not contain a valid install manifest
*/
function loadManifestFromZipReader(aZipReader) {
let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST);
let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
createInstance(Ci.nsIBufferedInputStream);
bis.init(zis, 4096);
try {
let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST);
let addon = loadManifestFromRDF(uri, bis);
addon._sourceBundle = aZipReader.file;
addon.size = 0;
let entries = aZipReader.findEntries(null);
while (entries.hasMore())
addon.size += aZipReader.getEntry(entries.getNext()).realSize;
return addon;
}
finally {
bis.close();
zis.close();
}
}
/**
* Creates a jar: URI for a file inside a ZIP file.
*
@ -1463,7 +1546,7 @@ var XPIProvider = {
// add-on will just be unavailable until we try again in a subsequent
// startup
ERROR("Failed to add add-on " + aId + " in " + aInstallLocation.name +
" to database");
" to database: " + e);
return false;
}
@ -2368,7 +2451,8 @@ const FIELDS_ADDON = "internal_id, id, location, version, type, internalName, "
"updateURL, updateKey, optionsURL, aboutURL, iconURL, " +
"defaultLocale, visible, active, userDisabled, appDisabled, " +
"pendingUninstall, descriptor, installDate, updateDate, " +
"applyBackgroundUpdates, bootstrap";
"applyBackgroundUpdates, bootstrap, skinnable, size, " +
"sourceURI, releaseNotesURI";
/**
* A helper function to log an SQL error.
@ -2493,15 +2577,19 @@ var XPIDatabase = {
_getTargetApplications: "SELECT addon_internal_id, id, minVersion, " +
"maxVersion FROM targetApplication WHERE " +
"addon_internal_id=:internal_id",
_getTargetPlatforms: "SELECT os, abi FROM targetPlatform WHERE " +
"addon_internal_id=:internal_id",
_readLocaleStrings: "SELECT locale_id, type, value FROM locale_strings " +
"WHERE locale_id=:id",
addAddonMetadata_addon: "INSERT INTO addon VALUES (NULL, :id, :location, " +
":version, :type, :internalName, :updateURL, " +
":updateKey, :optionsURL, :aboutURL, :iconURL, " +
":locale, :visible, :active, :userDisabled," +
" :appDisabled, 0, :descriptor, :installDate, " +
":updateDate, :applyBackgroundUpdates, :bootstrap)",
":locale, :visible, :active, :userDisabled, " +
":appDisabled, :pendingUninstall, :descriptor, " +
":installDate, :updateDate, :applyBackgroundUpdates, " +
":bootstrap, :skinnable, :size, :sourceURI, " +
":releaseNotesURI)",
addAddonMetadata_addon_locale: "INSERT INTO addon_locale VALUES " +
"(:internal_id, :name, :locale)",
addAddonMetadata_locale: "INSERT INTO locale (name, description, creator, " +
@ -2512,6 +2600,8 @@ var XPIDatabase = {
addAddonMetadata_targetApplication: "INSERT INTO targetApplication VALUES " +
"(:internal_id, :id, :minVersion, " +
":maxVersion)",
addAddonMetadata_targetPlatform: "INSERT INTO targetPlatform VALUES " +
"(:internal_id, :os, :abi)",
clearVisibleAddons: "UPDATE addon SET visible=0 WHERE id=:id",
updateAddonActive: "UPDATE addon SET active=:active WHERE " +
@ -2691,24 +2781,48 @@ var XPIDatabase = {
// Attempt to migrate data from a different (even future!) version of the
// database
try {
var stmt = this.connection.createStatement("SELECT id, location, " +
"userDisabled, installDate " +
"FROM addon");
var stmt = this.connection.createStatement("SELECT internal_id, id, " +
"location, userDisabled, " +
"installDate FROM addon");
for (let row in resultRows(stmt)) {
if (!(row.location in migrateData))
migrateData[row.location] = {};
migrateData[row.location][row.id] = {
internal_id: row.internal_id,
installDate: row.installDate,
userDisabled: row.userDisabled == 1
userDisabled: row.userDisabled == 1,
targetApplications: []
};
}
var taStmt = this.connection.createStatement("SELECT id, minVersion, " +
"maxVersion FROM " +
"targetApplication WHERE " +
"addon_internal_id=:internal_id");
for (let location in migrateData) {
for (let id in migrateData[location]) {
taStmt.params.internal_id = migrateData[location][id].internal_id;
delete migrateData[location][id].internal_id;
for (let row in resultRows(taStmt)) {
migrateData[location][id].targetApplications.push({
id: row.id,
minVersion: row.minVersion,
maxVersion: row.maxVersion
});
}
}
}
}
catch (e) {
// An error here means the schema is too different to read
ERROR("Error migrating data: " + e);
}
finally {
stmt.finalize();
if (taStmt)
taStmt.finalize();
if (stmt)
stmt.finalize();
}
this.connection.close();
this.initialized = false;
@ -2796,11 +2910,17 @@ var XPIDatabase = {
"pendingUninstall INTEGER, descriptor TEXT, " +
"installDate INTEGER, updateDate INTEGER, " +
"applyBackgroundUpdates INTEGER, " +
"bootstrap INTEGER, UNIQUE (id, location)");
"bootstrap INTEGER, skinnable INTEGER, " +
"size INTEGER, sourceURI TEXT, " +
"releaseNotesURI TEXT, UNIQUE (id, location)");
this.connection.createTable("targetApplication",
"addon_internal_id INTEGER, " +
"id TEXT, minVersion TEXT, maxVersion TEXT, " +
"UNIQUE (addon_internal_id, id)");
this.connection.createTable("targetPlatform",
"addon_internal_id INTEGER, " +
"os, abi TEXT, " +
"UNIQUE (addon_internal_id, os, abi)");
this.connection.createTable("addon_locale",
"addon_internal_id INTEGER, "+
"locale TEXT, locale_id INTEGER, " +
@ -2814,6 +2934,7 @@ var XPIDatabase = {
this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
"ON addon BEGIN " +
"DELETE FROM targetApplication WHERE addon_internal_id=old.internal_id; " +
"DELETE FROM targetPlatform WHERE addon_internal_id=old.internal_id; " +
"DELETE FROM addon_locale WHERE addon_internal_id=old.internal_id; " +
"DELETE FROM locale WHERE id=old.defaultLocale; " +
"END");
@ -2916,6 +3037,20 @@ var XPIDatabase = {
return [copyProperties(row, PROP_TARGETAPP) for each (row in resultRows(stmt))];
},
/**
* Synchronously reads the target platform entries for an add-on
*
* @param aAddon
* The DBAddonInternal to read the target platforms for
* @return an array of target platforms
*/
_getTargetPlatforms: function XPIDB__getTargetPlatforms(aAddon) {
let stmt = this.getStatement("_getTargetPlatforms");
stmt.params.internal_id = aAddon._internal_id;
return [copyRowProperties(row, ["os", "abi"]) for each (row in resultRows(stmt))];
},
/**
* Synchronously makes a DBAddonInternal from a storage row or returns one
* from the cache.
@ -2935,13 +3070,10 @@ var XPIDatabase = {
addon._internal_id = aRow.internal_id;
addon._installLocation = XPIProvider.installLocationsByName[aRow.location];
addon._descriptor = aRow.descriptor;
copyProperties(aRow, PROP_METADATA, addon);
addon._defaultLocale = aRow.defaultLocale;
addon.installDate = aRow.installDate;
addon.updateDate = aRow.updateDate;
["visible", "active", "userDisabled", "appDisabled",
"pendingUninstall", "applyBackgroundUpdates",
"bootstrap"].forEach(function(aProp) {
copyProperties(aRow, PROP_METADATA, addon);
copyProperties(aRow, DB_METADATA, addon);
DB_BOOL_METADATA.forEach(function(aProp) {
addon[aProp] = aRow[aProp] != 0;
});
this.addonCache[aRow.internal_id] = Components.utils.getWeakReference(addon);
@ -3056,6 +3188,27 @@ var XPIDatabase = {
handleError: asyncErrorLogger,
handleCompletion: function(aReason) {
readTargetPlatforms();
}
});
}
function readTargetPlatforms() {
delete aAddon.targetPlatforms;
aAddon.targetPlatforms = [];
let stmt = XPIDatabase.getStatement("_getTargetPlatforms");
stmt.params.internal_id = aAddon._internal_id;
stmt.executeAsync({
handleResult: function(aResults) {
let row = null;
while (row = aResults.getNextRow())
aAddon.targetPlatforms.push(copyRowProperties(row, ["os", "abi"]));
},
handleError: asyncErrorLogger,
handleCompletion: function(aReason) {
let callbacks = aAddon._pendingCallbacks;
delete aAddon._pendingCallbacks;
@ -3100,11 +3253,8 @@ var XPIDatabase = {
addon._descriptor = aRow.getResultByName("descriptor");
copyRowProperties(aRow, PROP_METADATA, addon);
addon._defaultLocale = aRow.getResultByName("defaultLocale");
addon.installDate = aRow.getResultByName("installDate");
addon.updateDate = aRow.getResultByName("updateDate");
["visible", "active", "userDisabled", "appDisabled",
"pendingUninstall", "applyBackgroundUpdates",
"bootstrap"].forEach(function(aProp) {
copyRowProperties(aRow, DB_METADATA, addon);
DB_BOOL_METADATA.forEach(function(aProp) {
addon[aProp] = aRow.getResultByName(aProp) != 0;
});
@ -3352,14 +3502,11 @@ var XPIDatabase = {
stmt.params.locale = insertLocale(aAddon.defaultLocale);
stmt.params.location = aAddon._installLocation.name;
stmt.params.descriptor = aDescriptor;
stmt.params.installDate = aAddon.installDate;
stmt.params.updateDate = aAddon.updateDate;
copyProperties(aAddon, PROP_METADATA, stmt.params);
["visible", "userDisabled", "appDisabled", "applyBackgroundUpdates",
"bootstrap"].forEach(function(aProp) {
copyProperties(aAddon, DB_METADATA, stmt.params);
DB_BOOL_METADATA.forEach(function(aProp) {
stmt.params[aProp] = aAddon[aProp] ? 1 : 0;
});
stmt.params.active = aAddon.active ? 1 : 0;
executeStatement(stmt);
let internal_id = this.connection.lastInsertRowID;
@ -3383,6 +3530,16 @@ var XPIDatabase = {
stmt.params.maxVersion = aApp.maxVersion;
executeStatement(stmt);
});
stmt = this.getStatement("addAddonMetadata_targetPlatform");
aAddon.targetPlatforms.forEach(function(aPlatform) {
stmt.params.internal_id = internal_id;
stmt.params.os = aPlatform.os;
stmt.params.abi = aPlatform.abi;
executeStatement(stmt);
});
this.commitTransaction();
}
catch (e) {
@ -3876,31 +4033,19 @@ AddonInstall.prototype = {
throw new Error("Missing install.rdf");
}
let zis = zipreader.getInputStream(FILE_INSTALL_MANIFEST);
let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
createInstance(Ci.nsIBufferedInputStream);
bis.init(zis, 4096);
this.addon = loadManifestFromZipReader(zipreader);
this.addon.sourceURI = this.sourceURL.spec;
this.addon._install = this;
try {
uri = buildJarURI(this.file, FILE_INSTALL_MANIFEST);
this.addon = loadManifestFromRDF(uri, bis);
this.addon._sourceBundle = this.file;
this.addon._install = this;
this.name = this.addon.selectedLocale.name;
this.type = this.addon.type;
this.version = this.addon.version;
this.name = this.addon.selectedLocale.name;
this.type = this.addon.type;
this.version = this.addon.version;
// Setting the iconURL to something inside the XPI locks the XPI and
// makes it impossible to delete on Windows.
//let newIcon = createWrapper(this.addon).iconURL;
//if (newIcon)
// this.iconURL = newIcon;
}
finally {
bis.close();
zis.close();
}
// Setting the iconURL to something inside the XPI locks the XPI and
// makes it impossible to delete on Windows.
//let newIcon = createWrapper(this.addon).iconURL;
//if (newIcon)
// this.iconURL = newIcon;
}
finally {
zipreader.close();
@ -4586,6 +4731,8 @@ AddonInternal.prototype = {
visible: false,
userDisabled: false,
appDisabled: false,
sourceURI: null,
releaseNotesURI: null,
get selectedLocale() {
if (this._selectedLocale)
@ -4705,6 +4852,11 @@ function DBAddonInternal() {
return this.targetApplications = XPIDatabase._getTargetApplications(this);
});
this.__defineGetter__("targetPlatforms", function() {
delete this.targetPlatforms;
return this.targetPlatforms = XPIDatabase._getTargetPlatforms(this);
});
this.__defineGetter__("locales", function() {
delete this.locales;
return this.locales = XPIDatabase._getLocales(this);
@ -4779,7 +4931,7 @@ function createWrapper(aAddon) {
function AddonWrapper(aAddon) {
["id", "version", "type", "isCompatible",
"providesUpdatesSecurely", "blocklistState", "appDisabled",
"userDisabled"].forEach(function(aProp) {
"userDisabled", "skinnable", "size"].forEach(function(aProp) {
this.__defineGetter__(aProp, function() aAddon[aProp]);
}, this);
@ -4793,6 +4945,14 @@ function AddonWrapper(aAddon) {
this.__defineGetter__(aProp, function() new Date(aAddon[aProp]));
}, this);
["sourceURI", "releaseNotesURI"].forEach(function(aProp) {
this.__defineGetter__(aProp, function() {
if (!aAddon[aProp])
return null;
return NetUtil.newURI(aAddon[aProp]);
});
}, this);
this.__defineGetter__("iconURL", function() {
if (aAddon.active && aAddon.iconURL)
return aAddon.iconURL;
@ -5090,7 +5250,6 @@ DirectoryInstallLocation.prototype = {
* Finds all the add-ons installed in this location.
*/
_readAddons: function DirInstallLocation__readAddons() {
try {
let entries = this._directory.directoryEntries
.QueryInterface(Ci.nsIDirectoryEnumerator);
let entry;
@ -5131,10 +5290,6 @@ DirectoryInstallLocation.prototype = {
this._DirToIDMap[entry.path] = id;
}
entries.close();
}
catch (e) {
ERROR(e);
}
},
/**

View File

@ -48,6 +48,17 @@ var addon4 = {
}]
};
var addon5 = {
id: "addon5@tests.mozilla.org",
version: "2.0",
name: "Test 5",
targetApplications: [{
id: "xpcshell@tests.mozilla.org",
minVersion: "0",
maxVersion: "0"
}]
};
const profileDir = gProfD.clone();
profileDir.append("extensions");
@ -67,7 +78,9 @@ function run_test() {
dest = profileDir.clone();
dest.append("addon4@tests.mozilla.org");
writeInstallRDFToDir(addon4, dest);
dest = profileDir.clone();
dest.append("addon5@tests.mozilla.org");
writeInstallRDFToDir(addon5, dest);
// Write out a minimal database
let dbfile = gProfD.clone();
@ -75,16 +88,19 @@ function run_test() {
let db = AM_Cc["@mozilla.org/storage/service;1"].
getService(AM_Ci.mozIStorageService).
openDatabase(dbfile);
db.createTable("addon", "id TEXT, location TEXT, active INTEGER, " +
"userDisabled INTEGER, installDate INTEGER, " +
"UNIQUE (id, location)");
let stmt = db.createStatement("INSERT INTO addon VALUES (:id, :location, " +
db.createTable("addon", "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"id TEXT, location TEXT, active INTEGER, " +
"userDisabled INTEGER, installDate INTEGER");
db.createTable("targetApplication", "addon_internal_id INTEGER, " +
"id TEXT, minVersion TEXT, maxVersion TEXT");
let stmt = db.createStatement("INSERT INTO addon VALUES (NULL, :id, :location, " +
":active, :userDisabled, :installDate)");
[["addon1@tests.mozilla.org", "app-profile", "1", "0", "0"],
["addon2@tests.mozilla.org", "app-profile", "0", "1", "0"],
["addon3@tests.mozilla.org", "app-profile", "1", "1", "0"],
["addon4@tests.mozilla.org", "app-profile", "0", "0", "0"]].forEach(function(a) {
["addon4@tests.mozilla.org", "app-profile", "0", "0", "0"],
["addon5@tests.mozilla.org", "app-profile", "1", "0", "0"]].forEach(function(a) {
stmt.params.id = a[0];
stmt.params.location = a[1];
stmt.params.active = a[2];
@ -93,6 +109,18 @@ function run_test() {
stmt.execute();
});
stmt.finalize();
// Add updated target application into for addon5
let internal_id = db.lastInsertRowID;
stmt = db.createStatement("INSERT INTO targetApplication VALUES " +
"(:internal_id, :id, :minVersion, :maxVersion)");
stmt.params.internal_id = internal_id;
stmt.params.id = "xpcshell@tests.mozilla.org";
stmt.params.minVersion = "0";
stmt.params.maxVersion = "1";
stmt.execute();
stmt.finalize();
db.close();
Services.prefs.setIntPref("extensions.databaseSchema", 100);
@ -100,19 +128,34 @@ function run_test() {
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
"addon2@tests.mozilla.org",
"addon3@tests.mozilla.org",
"addon4@tests.mozilla.org"], function([a1, a2, a3, a4]) {
"addon4@tests.mozilla.org",
"addon5@tests.mozilla.org"],
function([a1, a2, a3, a4, a5]) {
// addon1 was enabled in the database
do_check_neq(a1, null);
do_check_false(a1.userDisabled);
// addon1 was disabled in the database
do_check_false(a1.appDisabled);
do_check_true(a1.isActive);
// addon2 was disabled in the database
do_check_neq(a2, null);
do_check_true(a2.userDisabled);
// addon1 was pending-disable in the database
do_check_false(a2.appDisabled);
do_check_false(a2.isActive);
// addon3 was pending-disable in the database
do_check_neq(a3, null);
do_check_true(a3.userDisabled);
// addon1 was pending-enable in the database
do_check_false(a3.appDisabled);
do_check_false(a3.isActive);
// addon4 was pending-enable in the database
do_check_neq(a4, null);
do_check_false(a4.userDisabled);
do_check_false(a4.appDisabled);
do_check_true(a4.isActive);
// addon5 was enabled in the database but needed a compatibiltiy update
do_check_neq(a5, null);
do_check_false(a5.userDisabled);
do_check_false(a5.appDisabled);
do_check_true(a5.isActive);
do_test_finished();
});
}