mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			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"]),
 | 
						|
};
 |