272 lines
8.1 KiB
JavaScript
272 lines
8.1 KiB
JavaScript
/* 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/. */
|
|
|
|
import { Log } from "resource://gre/modules/Log.sys.mjs";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
XPCOMUtils.defineLazyServiceGetters(lazy, {
|
|
ThirdPartyUtil: ["@mozilla.org/thirdpartyutil;1", "mozIThirdPartyUtil"],
|
|
});
|
|
|
|
const XPINSTALL_MIMETYPE = "application/x-xpinstall";
|
|
|
|
const MSG_INSTALL_ENABLED = "WebInstallerIsInstallEnabled";
|
|
const MSG_INSTALL_ADDON = "WebInstallerInstallAddonFromWebpage";
|
|
const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
|
|
|
|
const SUPPORTED_XPI_SCHEMES = ["http", "https"];
|
|
|
|
var log = Log.repository.getLogger("AddonManager.InstallTrigger");
|
|
log.level =
|
|
Log.Level[
|
|
Services.prefs.getBoolPref("extensions.logging.enabled", false)
|
|
? "Warn"
|
|
: "Trace"
|
|
];
|
|
|
|
function CallbackObject(id, callback, mediator) {
|
|
this.id = id;
|
|
this.callback = callback;
|
|
this.callCallback = function (url, status) {
|
|
try {
|
|
this.callback(url, status);
|
|
} catch (e) {
|
|
log.warn("InstallTrigger callback threw an exception: " + e);
|
|
}
|
|
|
|
mediator._callbacks.delete(id);
|
|
};
|
|
}
|
|
|
|
function RemoteMediator(window) {
|
|
this._windowID = window.windowGlobalChild.innerWindowId;
|
|
|
|
this.mm = window.docShell.messageManager;
|
|
this.mm.addWeakMessageListener(MSG_INSTALL_CALLBACK, this);
|
|
|
|
this._lastCallbackID = 0;
|
|
this._callbacks = new Map();
|
|
}
|
|
|
|
RemoteMediator.prototype = {
|
|
receiveMessage(message) {
|
|
if (message.name == MSG_INSTALL_CALLBACK) {
|
|
let payload = message.data;
|
|
let callbackHandler = this._callbacks.get(payload.callbackID);
|
|
if (callbackHandler) {
|
|
callbackHandler.callCallback(payload.url, payload.status);
|
|
}
|
|
}
|
|
},
|
|
|
|
enabled() {
|
|
let params = {
|
|
mimetype: XPINSTALL_MIMETYPE,
|
|
};
|
|
return this.mm.sendSyncMessage(MSG_INSTALL_ENABLED, params)[0];
|
|
},
|
|
|
|
install(install, principal, callback, window) {
|
|
let callbackID = this._addCallback(callback);
|
|
|
|
install.mimetype = XPINSTALL_MIMETYPE;
|
|
install.triggeringPrincipal = principal;
|
|
install.callbackID = callbackID;
|
|
install.browsingContext = BrowsingContext.getFromWindow(window);
|
|
|
|
return Services.cpmm.sendSyncMessage(MSG_INSTALL_ADDON, install)[0];
|
|
},
|
|
|
|
_addCallback(callback) {
|
|
if (!callback || typeof callback != "function") {
|
|
return -1;
|
|
}
|
|
|
|
let callbackID = this._windowID + "-" + ++this._lastCallbackID;
|
|
let callbackObject = new CallbackObject(callbackID, callback, this);
|
|
this._callbacks.set(callbackID, callbackObject);
|
|
return callbackID;
|
|
},
|
|
|
|
QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
|
|
};
|
|
|
|
export function InstallTrigger() {}
|
|
|
|
InstallTrigger.prototype = {
|
|
// We've declared ourselves as providing the nsIDOMGlobalPropertyInitializer
|
|
// interface. This means that when the InstallTrigger property is gotten from
|
|
// the window that will createInstance this object and then call init(),
|
|
// passing the window were bound to. It will then automatically create the
|
|
// WebIDL wrapper (InstallTriggerImpl) for this object. This indirection is
|
|
// necessary because webidl does not (yet) support statics (bug 863952). See
|
|
// bug 926712 and then bug 1442360 for more details about this implementation.
|
|
init(window) {
|
|
this._window = window;
|
|
this._principal = window.document.nodePrincipal;
|
|
this._url = window.document.documentURIObject;
|
|
|
|
this._mediator = new RemoteMediator(window);
|
|
// If we can't set up IPC (e.g., because this is a top-level window or
|
|
// something), then don't expose InstallTrigger. The Window code handles
|
|
// that, if we throw an exception here.
|
|
},
|
|
|
|
enabled() {
|
|
return this._mediator.enabled(this._url.spec);
|
|
},
|
|
|
|
updateEnabled() {
|
|
return this.enabled();
|
|
},
|
|
|
|
install(installs, callback) {
|
|
if (Services.prefs.getBoolPref("xpinstall.userActivation.required", true)) {
|
|
if (!this._window.windowUtils.isHandlingUserInput) {
|
|
throw new this._window.Error(
|
|
"InstallTrigger.install can only be called from a user input handler"
|
|
);
|
|
}
|
|
}
|
|
|
|
let keys = Object.keys(installs);
|
|
if (keys.length > 1) {
|
|
throw new this._window.Error("Only one XPI may be installed at a time");
|
|
}
|
|
|
|
let item = installs[keys[0]];
|
|
|
|
if (typeof item === "string") {
|
|
item = { URL: item };
|
|
}
|
|
if (!item.URL) {
|
|
throw new this._window.Error(
|
|
"Missing URL property for '" + keys[0] + "'"
|
|
);
|
|
}
|
|
|
|
let url = this._resolveURL(item.URL);
|
|
if (!this._checkLoadURIFromScript(url)) {
|
|
throw new this._window.Error(
|
|
"Insufficient permissions to install: " + url.spec
|
|
);
|
|
}
|
|
|
|
if (!SUPPORTED_XPI_SCHEMES.includes(url.scheme)) {
|
|
Cu.reportError(
|
|
`InstallTrigger call disallowed on install url with unsupported scheme: ${JSON.stringify(
|
|
{
|
|
installPrincipal: this._principal.spec,
|
|
installURL: url.spec,
|
|
}
|
|
)}`
|
|
);
|
|
throw new this._window.Error(`Unsupported scheme`);
|
|
}
|
|
|
|
let iconUrl = null;
|
|
if (item.IconURL) {
|
|
iconUrl = this._resolveURL(item.IconURL);
|
|
if (!this._checkLoadURIFromScript(iconUrl)) {
|
|
iconUrl = null; // If page can't load the icon, just ignore it
|
|
}
|
|
}
|
|
|
|
const getTriggeringSource = () => {
|
|
let url;
|
|
let host;
|
|
try {
|
|
if (this._url?.schemeIs("http") || this._url?.schemeIs("https")) {
|
|
url = this._url.spec;
|
|
host = this._url.host;
|
|
} else if (this._url?.schemeIs("blob")) {
|
|
// For a blob url, we keep the blob url as the sourceURL and
|
|
// we pick the related sourceHost from either the principal
|
|
// or the precursorPrincipal (if the principal is a null principal
|
|
// and the precursor one is defined).
|
|
url = this._url.spec;
|
|
host =
|
|
this._principal.isNullPrincipal &&
|
|
this._principal.precursorPrincipal
|
|
? this._principal.precursorPrincipal.host
|
|
: this._principal.host;
|
|
} else if (!this._principal.isNullPrincipal) {
|
|
url = this._principal.exposableSpec;
|
|
host = this._principal.host;
|
|
} else if (this._principal.precursorPrincipal) {
|
|
url = this._principal.precursorPrincipal.exposableSpec;
|
|
host = this._principal.precursorPrincipal.host;
|
|
} else {
|
|
// Fallback to this._url as last resort.
|
|
url = this._url.spec;
|
|
host = this._url.host;
|
|
}
|
|
} catch (err) {
|
|
Cu.reportError(err);
|
|
}
|
|
// Fallback to an empty string if url and host are still undefined.
|
|
return {
|
|
sourceURL: url || "",
|
|
sourceHost: host || "",
|
|
};
|
|
};
|
|
|
|
const { sourceHost, sourceURL } = getTriggeringSource();
|
|
|
|
let installData = {
|
|
uri: url.spec,
|
|
hash: item.Hash || null,
|
|
name: item.name,
|
|
icon: iconUrl ? iconUrl.spec : null,
|
|
method: "installTrigger",
|
|
sourceHost,
|
|
sourceURL,
|
|
hasCrossOriginAncestor: lazy.ThirdPartyUtil.isThirdPartyWindow(
|
|
this._window
|
|
),
|
|
};
|
|
|
|
return this._mediator.install(
|
|
installData,
|
|
this._principal,
|
|
callback,
|
|
this._window
|
|
);
|
|
},
|
|
|
|
startSoftwareUpdate(url) {
|
|
let filename = Services.io.newURI(url).QueryInterface(Ci.nsIURL).filename;
|
|
let args = {};
|
|
args[filename] = { URL: url };
|
|
return this.install(args);
|
|
},
|
|
|
|
installChrome(type, url) {
|
|
return this.startSoftwareUpdate(url);
|
|
},
|
|
|
|
_resolveURL(url) {
|
|
return Services.io.newURI(url, null, this._url);
|
|
},
|
|
|
|
_checkLoadURIFromScript(uri) {
|
|
let secman = Services.scriptSecurityManager;
|
|
try {
|
|
secman.checkLoadURIWithPrincipal(
|
|
this._principal,
|
|
uri,
|
|
secman.DISALLOW_INHERIT_PRINCIPAL
|
|
);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
classID: Components.ID("{9df8ef2b-94da-45c9-ab9f-132eb55fddf1}"),
|
|
contractID: "@mozilla.org/addons/installtrigger;1",
|
|
QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]),
|
|
};
|