diff --git a/toolkit/mozapps/extensions/XPIProvider.jsm b/toolkit/mozapps/extensions/XPIProvider.jsm index 69f2f8ff67cd..9e35fcff0ca9 100644 --- a/toolkit/mozapps/extensions/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/XPIProvider.jsm @@ -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); - } }, /** diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_migrate2.js b/toolkit/mozapps/extensions/test/xpcshell/test_migrate2.js index 68a314ee06d2..8f03b4a1cc05 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_migrate2.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate2.js @@ -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(); }); }