Bug 846019 - Part 1: amIAddonManager: Map URIs to AddonIDs. r=bmcbride

This commit is contained in:
Nils Maier 2013-06-14 22:48:06 -04:00
parent 3d36dc133c
commit d52f715bfa
8 changed files with 504 additions and 2 deletions

View File

@ -1455,6 +1455,35 @@ var AddonManagerInternal = {
this.getInstallsByTypes(null, aCallback);
},
/**
* Synchronously map a URI to the corresponding Addon ID.
*
* Mappable URIs are limited to in-application resources belonging to the
* add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
* but do not include URIs from meta data, such as the add-on homepage.
*
* @param aURI
* nsIURI to map to an addon id
* @return string containing the Addon ID or null
* @see amIAddonManager.mapURIToAddonID
*/
mapURIToAddonID: function AMI_mapURIToAddonID(aURI) {
if (!(aURI instanceof Ci.nsIURI)) {
throw Components.Exception("aURI is not a nsIURI",
Cr.NS_ERROR_INVALID_ARG);
}
// Try all providers
let providers = this.providers.slice(0);
for (let provider of providers) {
var id = callProvider(provider, "mapURIToAddonID", null, aURI);
if (id !== null) {
return id;
}
}
return null;
},
/**
* Checks whether installation is enabled for a particular mimetype.
*
@ -2340,6 +2369,10 @@ this.AddonManager = {
AddonManagerInternal.getAllInstalls(aCallback);
},
mapURIToAddonID: function AM_mapURIToAddonID(aURI) {
return AddonManagerInternal.mapURIToAddonID(aURI);
},
isInstallEnabled: function AM_isInstallEnabled(aType) {
return AddonManagerInternal.isInstallEnabled(aType);
},

View File

@ -25,6 +25,18 @@ XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyServiceGetter(this,
"ChromeRegistry",
"@mozilla.org/chrome/chrome-registry;1",
"nsIChromeRegistry");
XPCOMUtils.defineLazyServiceGetter(this,
"ResProtocolHandler",
"@mozilla.org/network/protocol;1?name=resource",
"nsIResProtocolHandler");
const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
"initWithPath");
const PREF_DB_SCHEMA = "extensions.databaseSchema";
const PREF_INSTALL_CACHE = "extensions.installCache";
const PREF_BOOTSTRAP_ADDONS = "extensions.bootstrappedAddons";
@ -1507,6 +1519,123 @@ var XPIProvider = {
// Count of unpacked add-ons
unpackedAddons: 0,
/**
* Adds or updates a URI mapping for an Addon.id.
*
* Mappings should not be removed at any point. This is so that the mappings
* will be still valid after an add-on gets disabled or uninstalled, as
* consumers may still have URIs of (leaked) resources they want to map.
*/
_addURIMapping: function XPI__addURIMapping(aID, aFile) {
try {
// Always use our own mechanics instead of nsIIOService.newFileURI, so
// that we can be sure to map things as we want them mapped.
let uri = this._resolveURIToFile(getURIForResourceInFile(aFile, "."));
if (!uri) {
throw new Error("Cannot resolve");
}
this._ensureURIMappings();
this._uriMappings[aID] = uri.spec;
}
catch (ex) {
WARN("Failed to add URI mapping", ex);
}
},
/**
* Ensures that the URI to Addon mappings are available.
*
* The function will add mappings for all non-bootstrapped but enabled
* add-ons.
* Bootstrapped add-on mappings will be added directly when the bootstrap
* scope get loaded. (See XPIProvider._addURIMapping() and callers)
*/
_ensureURIMappings: function XPI__ensureURIMappings() {
if (this._uriMappings) {
return;
}
// XXX Convert to Map(), once it gets stable with stable iterators
this._uriMappings = Object.create(null);
// XXX Convert to Set(), once it gets stable with stable iterators
let enabled = Object.create(null);
for (let a of this.enabledAddons.split(",")) {
a = decodeURIComponent(a.split(":")[0]);
enabled[a] = null;
}
let cache = JSON.parse(Prefs.getCharPref(PREF_INSTALL_CACHE, "[]"));
for (let loc of cache) {
for (let [id, val] in Iterator(loc.addons)) {
if (!(id in enabled)) {
continue;
}
let file = new nsIFile(val.descriptor);
let spec = Services.io.newFileURI(file).spec;
this._uriMappings[id] = spec;
}
}
},
/**
* Resolve a URI back to physical file.
*
* Of course, this works only for URIs pointing to local resources.
*
* @param aURI
* URI to resolve
* @return
* resolved nsIFileURL
*/
_resolveURIToFile: function XPI__resolveURIToFile(aURI) {
switch (aURI.scheme) {
case "jar":
case "file":
if (aURI instanceof Ci.nsIJARURI) {
return this._resolveURIToFile(aURI.JARFile);
}
return aURI;
case "chrome":
aURI = ChromeRegistry.convertChromeURL(aURI);
return this._resolveURIToFile(aURI);
case "resource":
aURI = Services.io.newURI(ResProtocolHandler.resolveURI(aURI), null,
null);
return this._resolveURIToFile(aURI);
case "view-source":
aURI = Services.io.newURI(aURI.path, null, null);
return this._resolveURIToFile(aURI);
case "about":
if (aURI.spec == "about:blank") {
// Do not attempt to map about:blank
return null;
}
let chan;
try {
chan = Services.io.newChannelFromURI(aURI);
}
catch (ex) {
return null;
}
// Avoid looping
if (chan.URI.equals(aURI)) {
return null;
}
// We want to clone the channel URI to avoid accidentially keeping
// unnecessary references to the channel or implementation details
// around.
return this._resolveURIToFile(chan.URI.clone());
default:
return null;
}
},
/**
* Starts the XPI provider initializes the install locations and prefs.
*
@ -1732,6 +1861,9 @@ var XPIProvider = {
// This is needed to allow xpcshell tests to simulate a restart
this.extensionsActive = false;
// Remove URI mappings again
delete this._uriMappings;
if (gLazyObjectsLoaded) {
XPIDatabase.shutdown(function shutdownCallback() {
Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
@ -3358,6 +3490,34 @@ var XPIProvider = {
aCallback(results);
},
/**
* Synchronously map a URI to the corresponding Addon ID.
*
* Mappable URIs are limited to in-application resources belonging to the
* add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
* but do not include URIs from meta data, such as the add-on homepage.
*
* @param aURI
* nsIURI to map or null
* @return string containing the Addon ID
* @see AddonManager.mapURIToAddonID
* @see amIAddonManager.mapURIToAddonID
*/
mapURIToAddonID: function XPI_mapURIToAddonID(aURI) {
this._ensureURIMappings();
let resolved = this._resolveURIToFile(aURI);
if (!resolved) {
return null;
}
resolved = resolved.spec;
for (let [id, spec] in Iterator(this._uriMappings)) {
if (resolved.startsWith(spec)) {
return id;
}
}
return null;
},
/**
* Called when a new add-on has been enabled when only one add-on of that type
* can be enabled.
@ -3722,6 +3882,9 @@ var XPIProvider = {
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
createInstance(Ci.mozIJSSubScriptLoader);
// Add a mapping for XPIProvider.mapURIToAddonID
this._addURIMapping(aId, aFile);
try {
// Copy the reason values from the global object into the bootstrap scope.
for (let name in BOOTSTRAP_REASONS)

View File

@ -53,6 +53,14 @@ amManager.prototype = {
AddonManagerPrivate.startup();
},
/**
* @see amIAddonManager.idl
*/
mapURIToAddonID: function AMC_mapURIToAddonID(uri, id) {
id.value = AddonManager.mapURIToAddonID(uri);
return !!id.value;
},
/**
* @see amIWebInstaller.idl
*/
@ -201,7 +209,8 @@ amManager.prototype = {
return gSingleton.QueryInterface(aIid);
}
},
QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstaller,
QueryInterface: XPCOMUtils.generateQI([Ci.amIAddonManager,
Ci.amIWebInstaller,
Ci.nsITimerCallback,
Ci.nsIObserver])
};

View File

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
interface nsIURI;
/**
* A service to make some AddonManager functionality available to C++ callers.
* Javascript callers should still use AddonManager.jsm directly.
*/
[scriptable, function, uuid(7b45d82d-7ad5-48d7-9b05-f32eb9818cd4)]
interface amIAddonManager : nsISupports
{
/**
* Synchronously map a URI to the corresponding Addon ID.
*
* Mappable URIs are limited to in-application resources belonging to the
* add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
* but do not include URIs from meta data, such as the add-on homepage.
*
* @param aURI
* The nsIURI to map
* @return
* true if the URI has been mapped successfully to an Addon ID
*/
boolean mapURIToAddonID(in nsIURI aURI, out AUTF8String aID);
};

View File

@ -7,6 +7,7 @@
TEST_DIRS += ['test']
XPIDL_SOURCES += [
'amIAddonManager.idl',
'amIInstallTrigger.idl',
'amIWebInstallListener.idl',
'amIWebInstaller.idl',

View File

@ -0,0 +1,263 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
// This verifies that add-ons URIs can be mapped to add-on IDs
//
Components.utils.import("resource://gre/modules/Services.jsm");
// Enable loading extensions from the user scopes
Services.prefs.setIntPref("extensions.enabledScopes",
AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
const profileDir = gProfD.clone();
profileDir.append("extensions");
const userExtDir = gProfD.clone();
userExtDir.append("extensions2");
userExtDir.append(gAppInfo.ID);
registerDirectory("XREUSysExt", userExtDir.parent);
Components.utils.import("resource://testing-common/httpd.js");
var testserver;
function TestProvider(result) {
this.result = result;
}
TestProvider.prototype = {
uri: Services.io.newURI("hellow://world", null, null),
id: "valid@id",
startup: function() {},
shutdown: function() {},
mapURIToAddonID: function(aURI) {
if (aURI.spec === this.uri.spec) {
return this.id;
}
throw Components.Exception("Not mapped", result);
}
};
function TestProviderNoMap() {}
TestProviderNoMap.prototype = {
startup: function() {},
shutdown: function() {}
};
function check_mapping(uri, id) {
do_check_eq(AddonManager.mapURIToAddonID(uri), id);
let svc = Components.classes["@mozilla.org/addons/integration;1"].
getService(Components.interfaces.amIAddonManager);
let val = {};
do_check_true(svc.mapURIToAddonID(uri, val));
do_check_eq(val.value, id);
}
function resetPrefs() {
Services.prefs.setIntPref("bootstraptest.active_version", -1);
}
function waitForPref(aPref, aCallback) {
function prefChanged() {
Services.prefs.removeObserver(aPref, prefChanged);
aCallback();
}
Services.prefs.addObserver(aPref, prefChanged, false);
}
function getActiveVersion() {
return Services.prefs.getIntPref("bootstraptest.active_version");
}
function run_test() {
do_test_pending();
resetPrefs();
// Create and configure the HTTP server.
testserver = new HttpServer();
testserver.registerDirectory("/addons/", do_get_file("addons"));
testserver.start(4444);
startupManager();
run_test_nomapping();
}
function run_test_nomapping() {
do_check_eq(AddonManager.mapURIToAddonID(TestProvider.prototype.uri), null);
try {
let svc = Components.classes["@mozilla.org/addons/integration;1"].
getService(Components.interfaces.amIAddonManager);
let val = {};
do_check_false(svc.mapURIToAddonID(TestProvider.prototype.uri, val));
}
catch (ex) {
do_throw(ex);
}
run_test_1();
}
// Tests that add-on URIs are mappable after an install
function run_test_1() {
prepare_test({ }, [
"onNewInstall"
]);
AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), function(install) {
ensure_test_completed();
let addon = install.addon;
prepare_test({
"bootstrap1@tests.mozilla.org": [
["onInstalling", false],
"onInstalled"
]
}, [
"onInstallStarted",
"onInstallEnded",
], function() {
let uri = addon.getResourceURI(".");
check_mapping(uri, addon.id);
waitForPref("bootstraptest.active_version", function() {
run_test_2(uri);
});
});
install.install();
});
}
// Tests that add-on URIs are still mappable, even after the add-on gets
// disabled in-session.
function run_test_2(uri) {
AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
prepare_test({
"bootstrap1@tests.mozilla.org": [
["onDisabling", false],
"onDisabled"
]
});
b1.userDisabled = true;
ensure_test_completed();
AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(newb1) {
do_check_true(newb1.userDisabled);
check_mapping(uri, newb1.id);
run_test_3(uri);
});
});
}
// Tests that add-on URIs aren't mappable if the add-on was never started in a
// session
function run_test_3(uri) {
restartManager();
do_check_eq(AddonManager.mapURIToAddonID(uri), null);
run_test_4();
}
// Tests that add-on URIs are mappable after a restart + reenable
function run_test_4() {
restartManager();
AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
prepare_test({
"bootstrap1@tests.mozilla.org": [
["onEnabling", false],
"onEnabled"
]
});
b1.userDisabled = false;
ensure_test_completed();
AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(newb1) {
let uri = newb1.getResourceURI(".");
check_mapping(uri, newb1.id);
run_test_5();
});
});
}
// Tests that add-on URIs are mappable after a restart
function run_test_5() {
restartManager();
AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
let uri = b1.getResourceURI(".");
check_mapping(uri, b1.id);
run_test_invalidarg();
});
}
// Tests that the AddonManager will bail when mapURIToAddonID is called with an
// invalid argument
function run_test_invalidarg() {
restartManager();
let tests = [undefined,
null,
1,
"string",
"chrome://global/content/",
function() {}
];
for (var test of tests) {
try {
AddonManager.mapURIToAddonID(test);
throw new Error("Shouldn't be able to map the URI in question");
}
catch (ex if ex.result) {
do_check_eq(ex.result, Components.results.NS_ERROR_INVALID_ARG);
}
catch (ex) {
do_throw(ex);
}
}
run_test_provider();
}
// Tests that custom providers are correctly handled
function run_test_provider() {
restartManager();
const provider = new TestProvider(Components.results.NS_ERROR_NOT_AVAILABLE);
AddonManagerPrivate.registerProvider(provider);
check_mapping(provider.uri, provider.id);
let u2 = provider.uri.clone();
u2.path = "notmapped";
do_check_eq(AddonManager.mapURIToAddonID(u2), null);
AddonManagerPrivate.unregisterProvider(provider);
run_test_provider_nomap();
}
// Tests that custom providers are correctly handled, even not implementing
// mapURIToAddonID
function run_test_provider_nomap() {
restartManager();
const provider = new TestProviderNoMap();
AddonManagerPrivate.registerProvider(provider);
do_check_eq(AddonManager.mapURIToAddonID(TestProvider.prototype.uri), null);
AddonManagerPrivate.unregisterProvider(provider);
do_test_finished();
}

View File

@ -8,7 +8,8 @@ const IGNORE = ["escapeAddonURI", "shouldAutoUpdate", "getStartupChanges",
"addTypeListener", "removeTypeListener",
"addAddonListener", "removeAddonListener",
"addInstallListener", "removeInstallListener",
"addManagerListener", "removeManagerListener"];
"addManagerListener", "removeManagerListener",
"mapURIToAddonID"];
const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
"AddonScreenshot", "AddonType", "startup", "shutdown",

View File

@ -187,6 +187,9 @@ skip-if = os == "android"
[test_locked2.js]
[test_locked_strictcompat.js]
[test_manifest.js]
[test_mapURIToAddonID.js]
# Same as test_bootstrap.js
skip-if = os == "android"
[test_migrate1.js]
[test_migrate2.js]
[test_migrate3.js]