forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			487 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			487 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | 
						|
/* vim: set sts=2 sw=2 et tw=80: */
 | 
						|
/* 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/. */
 | 
						|
"use strict";
 | 
						|
 | 
						|
/**
 | 
						|
 * This module contains extension testing helper logic which is common
 | 
						|
 * between all test suites.
 | 
						|
 */
 | 
						|
 | 
						|
/* exported ExtensionTestCommon, MockExtension */
 | 
						|
 | 
						|
var EXPORTED_SYMBOLS = ["ExtensionTestCommon", "MockExtension"];
 | 
						|
 | 
						|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | 
						|
const { XPCOMUtils } = ChromeUtils.import(
 | 
						|
  "resource://gre/modules/XPCOMUtils.jsm"
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyGlobalGetters(this, ["TextEncoder"]);
 | 
						|
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  this,
 | 
						|
  "AddonManager",
 | 
						|
  "resource://gre/modules/AddonManager.jsm"
 | 
						|
);
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  this,
 | 
						|
  "Extension",
 | 
						|
  "resource://gre/modules/Extension.jsm"
 | 
						|
);
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  this,
 | 
						|
  "ExtensionParent",
 | 
						|
  "resource://gre/modules/ExtensionParent.jsm"
 | 
						|
);
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  this,
 | 
						|
  "ExtensionPermissions",
 | 
						|
  "resource://gre/modules/ExtensionPermissions.jsm"
 | 
						|
);
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  this,
 | 
						|
  "FileUtils",
 | 
						|
  "resource://gre/modules/FileUtils.jsm"
 | 
						|
);
 | 
						|
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
 | 
						|
 | 
						|
XPCOMUtils.defineLazyGetter(
 | 
						|
  this,
 | 
						|
  "apiManager",
 | 
						|
  () => ExtensionParent.apiManager
 | 
						|
);
 | 
						|
 | 
						|
const { ExtensionCommon } = ChromeUtils.import(
 | 
						|
  "resource://gre/modules/ExtensionCommon.jsm"
 | 
						|
);
 | 
						|
const { ExtensionUtils } = ChromeUtils.import(
 | 
						|
  "resource://gre/modules/ExtensionUtils.jsm"
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  this,
 | 
						|
  "uuidGen",
 | 
						|
  "@mozilla.org/uuid-generator;1",
 | 
						|
  "nsIUUIDGenerator"
 | 
						|
);
 | 
						|
 | 
						|
const { flushJarCache } = ExtensionUtils;
 | 
						|
 | 
						|
const { instanceOf } = ExtensionCommon;
 | 
						|
 | 
						|
XPCOMUtils.defineLazyGetter(this, "console", () =>
 | 
						|
  ExtensionCommon.getConsole()
 | 
						|
);
 | 
						|
 | 
						|
var ExtensionTestCommon;
 | 
						|
 | 
						|
/**
 | 
						|
 * A skeleton Extension-like object, used for testing, which installs an
 | 
						|
 * add-on via the add-on manager when startup() is called, and
 | 
						|
 * uninstalles it on shutdown().
 | 
						|
 *
 | 
						|
 * @param {string} id
 | 
						|
 * @param {nsIFile} file
 | 
						|
 * @param {nsIURI} rootURI
 | 
						|
 * @param {string} installType
 | 
						|
 */
 | 
						|
class MockExtension {
 | 
						|
  constructor(file, rootURI, addonData) {
 | 
						|
    this.id = null;
 | 
						|
    this.file = file;
 | 
						|
    this.rootURI = rootURI;
 | 
						|
    this.installType = addonData.useAddonManager;
 | 
						|
    this.addonData = addonData;
 | 
						|
    this.addon = null;
 | 
						|
 | 
						|
    let promiseEvent = eventName =>
 | 
						|
      new Promise(resolve => {
 | 
						|
        let onstartup = async (msg, extension) => {
 | 
						|
          this.maybeSetID(extension.rootURI, extension.id);
 | 
						|
          if (!this.id && this.addonPromise) {
 | 
						|
            await this.addonPromise;
 | 
						|
          }
 | 
						|
 | 
						|
          if (extension.id == this.id) {
 | 
						|
            apiManager.off(eventName, onstartup);
 | 
						|
            this._extension = extension;
 | 
						|
            resolve(extension);
 | 
						|
          }
 | 
						|
        };
 | 
						|
        apiManager.on(eventName, onstartup);
 | 
						|
      });
 | 
						|
 | 
						|
    this._extension = null;
 | 
						|
    this._extensionPromise = promiseEvent("startup");
 | 
						|
    this._readyPromise = promiseEvent("ready");
 | 
						|
    this._uninstallPromise = promiseEvent("uninstall-complete");
 | 
						|
  }
 | 
						|
 | 
						|
  maybeSetID(uri, id) {
 | 
						|
    if (
 | 
						|
      !this.id &&
 | 
						|
      uri instanceof Ci.nsIJARURI &&
 | 
						|
      uri.JARFile.QueryInterface(Ci.nsIFileURL).file.equals(this.file)
 | 
						|
    ) {
 | 
						|
      this.id = id;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  testMessage(...args) {
 | 
						|
    return this._extension.testMessage(...args);
 | 
						|
  }
 | 
						|
 | 
						|
  get tabManager() {
 | 
						|
    return this._extension.tabManager;
 | 
						|
  }
 | 
						|
 | 
						|
  on(...args) {
 | 
						|
    this._extensionPromise.then(extension => {
 | 
						|
      extension.on(...args);
 | 
						|
    });
 | 
						|
    // Extension.jsm emits a "startup" event on |extension| before emitting the
 | 
						|
    // "startup" event on |apiManager|. Trigger the "startup" event anyway, to
 | 
						|
    // make sure that the MockExtension behaves like an Extension with regards
 | 
						|
    // to the startup event.
 | 
						|
    if (args[0] === "startup" && !this._extension) {
 | 
						|
      this._extensionPromise.then(extension => {
 | 
						|
        args[1]("startup", extension);
 | 
						|
      });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  off(...args) {
 | 
						|
    this._extensionPromise.then(extension => {
 | 
						|
      extension.off(...args);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  _setIncognitoOverride() {
 | 
						|
    let { addonData } = this;
 | 
						|
    if (addonData && addonData.incognitoOverride) {
 | 
						|
      try {
 | 
						|
        let { id } = addonData.manifest.applications.gecko;
 | 
						|
        if (id) {
 | 
						|
          return ExtensionTestCommon.setIncognitoOverride({ id, addonData });
 | 
						|
        }
 | 
						|
      } catch (e) {}
 | 
						|
      throw new Error(
 | 
						|
        "Extension ID is required for setting incognito permission."
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async startup() {
 | 
						|
    await this._setIncognitoOverride();
 | 
						|
 | 
						|
    if (this.installType == "temporary") {
 | 
						|
      return AddonManager.installTemporaryAddon(this.file).then(async addon => {
 | 
						|
        this.addon = addon;
 | 
						|
        this.id = addon.id;
 | 
						|
        return this._readyPromise;
 | 
						|
      });
 | 
						|
    } else if (this.installType == "permanent") {
 | 
						|
      this.addonPromise = new Promise(resolve => {
 | 
						|
        this.resolveAddon = resolve;
 | 
						|
      });
 | 
						|
      let install = await AddonManager.getInstallForFile(this.file);
 | 
						|
      return new Promise((resolve, reject) => {
 | 
						|
        let listener = {
 | 
						|
          onInstallFailed: reject,
 | 
						|
          onInstallEnded: async (install, newAddon) => {
 | 
						|
            this.addon = newAddon;
 | 
						|
            this.id = newAddon.id;
 | 
						|
            this.resolveAddon(newAddon);
 | 
						|
            resolve(this._readyPromise);
 | 
						|
          },
 | 
						|
        };
 | 
						|
 | 
						|
        install.addListener(listener);
 | 
						|
        install.install();
 | 
						|
      });
 | 
						|
    }
 | 
						|
    throw new Error("installType must be one of: temporary, permanent");
 | 
						|
  }
 | 
						|
 | 
						|
  shutdown() {
 | 
						|
    this.addon.uninstall();
 | 
						|
    return this.cleanupGeneratedFile();
 | 
						|
  }
 | 
						|
 | 
						|
  cleanupGeneratedFile() {
 | 
						|
    return this._extensionPromise
 | 
						|
      .then(extension => {
 | 
						|
        return extension.broadcast("Extension:FlushJarCache", {
 | 
						|
          path: this.file.path,
 | 
						|
        });
 | 
						|
      })
 | 
						|
      .then(() => {
 | 
						|
        return OS.File.remove(this.file.path);
 | 
						|
      });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function provide(obj, keys, value, override = false) {
 | 
						|
  if (keys.length == 1) {
 | 
						|
    if (!(keys[0] in obj) || override) {
 | 
						|
      obj[keys[0]] = value;
 | 
						|
    }
 | 
						|
  } else {
 | 
						|
    if (!(keys[0] in obj)) {
 | 
						|
      obj[keys[0]] = {};
 | 
						|
    }
 | 
						|
    provide(obj[keys[0]], keys.slice(1), value, override);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
ExtensionTestCommon = class ExtensionTestCommon {
 | 
						|
  /**
 | 
						|
   * This code is designed to make it easy to test a WebExtension
 | 
						|
   * without creating a bunch of files. Everything is contained in a
 | 
						|
   * single JS object.
 | 
						|
   *
 | 
						|
   * Properties:
 | 
						|
   *   "background": "<JS code>"
 | 
						|
   *     A script to be loaded as the background script.
 | 
						|
   *     The "background" section of the "manifest" property is overwritten
 | 
						|
   *     if this is provided.
 | 
						|
   *   "manifest": {...}
 | 
						|
   *     Contents of manifest.json
 | 
						|
   *   "files": {"filename1": "contents1", ...}
 | 
						|
   *     Data to be included as files. Can be referenced from the manifest.
 | 
						|
   *     If a manifest file is provided here, it takes precedence over
 | 
						|
   *     a generated one. Always use "/" as a directory separator.
 | 
						|
   *     Directories should appear here only implicitly (as a prefix
 | 
						|
   *     to file names)
 | 
						|
   *
 | 
						|
   * To make things easier, the value of "background" and "files"[] can
 | 
						|
   * be a function, which is converted to source that is run.
 | 
						|
   *
 | 
						|
   * @param {object} data
 | 
						|
   * @returns {object}
 | 
						|
   */
 | 
						|
  static generateFiles(data) {
 | 
						|
    let files = {};
 | 
						|
 | 
						|
    Object.assign(files, data.files);
 | 
						|
 | 
						|
    let manifest = data.manifest;
 | 
						|
    if (!manifest) {
 | 
						|
      manifest = {};
 | 
						|
    }
 | 
						|
 | 
						|
    provide(manifest, ["name"], "Generated extension");
 | 
						|
    provide(manifest, ["manifest_version"], 2);
 | 
						|
    provide(manifest, ["version"], "1.0");
 | 
						|
 | 
						|
    if (data.background) {
 | 
						|
      let bgScript = uuidGen.generateUUID().number + ".js";
 | 
						|
 | 
						|
      provide(manifest, ["background", "scripts"], [bgScript], true);
 | 
						|
      files[bgScript] = data.background;
 | 
						|
    }
 | 
						|
 | 
						|
    provide(files, ["manifest.json"], JSON.stringify(manifest));
 | 
						|
 | 
						|
    for (let filename in files) {
 | 
						|
      let contents = files[filename];
 | 
						|
      if (typeof contents == "function") {
 | 
						|
        files[filename] = this.serializeScript(contents);
 | 
						|
      } else if (
 | 
						|
        typeof contents != "string" &&
 | 
						|
        !instanceOf(contents, "ArrayBuffer")
 | 
						|
      ) {
 | 
						|
        files[filename] = JSON.stringify(contents);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return files;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Write an xpi file to disk for a webextension.
 | 
						|
   * The generated extension is stored in the system temporary directory,
 | 
						|
   * and an nsIFile object pointing to it is returned.
 | 
						|
   *
 | 
						|
   * @param {object} data In the format handled by generateFiles.
 | 
						|
   * @returns {nsIFile}
 | 
						|
   */
 | 
						|
  static generateXPI(data) {
 | 
						|
    let files = this.generateFiles(data);
 | 
						|
    return this.generateZipFile(files);
 | 
						|
  }
 | 
						|
 | 
						|
  static generateZipFile(files, baseName = "generated-extension.xpi") {
 | 
						|
    let ZipWriter = Components.Constructor(
 | 
						|
      "@mozilla.org/zipwriter;1",
 | 
						|
      "nsIZipWriter"
 | 
						|
    );
 | 
						|
    let zipW = new ZipWriter();
 | 
						|
 | 
						|
    let file = FileUtils.getFile("TmpD", [baseName]);
 | 
						|
    file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
 | 
						|
 | 
						|
    const MODE_WRONLY = 0x02;
 | 
						|
    const MODE_TRUNCATE = 0x20;
 | 
						|
    zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
 | 
						|
 | 
						|
    // Needs to be in microseconds for some reason.
 | 
						|
    let time = Date.now() * 1000;
 | 
						|
 | 
						|
    function generateFile(filename) {
 | 
						|
      let components = filename.split("/");
 | 
						|
      let path = "";
 | 
						|
      for (let component of components.slice(0, -1)) {
 | 
						|
        path += component + "/";
 | 
						|
        if (!zipW.hasEntry(path)) {
 | 
						|
          zipW.addEntryDirectory(path, time, false);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    for (let filename in files) {
 | 
						|
      let script = files[filename];
 | 
						|
      if (!instanceOf(script, "ArrayBuffer")) {
 | 
						|
        script = new TextEncoder("utf-8").encode(script).buffer;
 | 
						|
      }
 | 
						|
 | 
						|
      let stream = Cc[
 | 
						|
        "@mozilla.org/io/arraybuffer-input-stream;1"
 | 
						|
      ].createInstance(Ci.nsIArrayBufferInputStream);
 | 
						|
      stream.setData(script, 0, script.byteLength);
 | 
						|
 | 
						|
      generateFile(filename);
 | 
						|
      zipW.addEntryStream(filename, time, 0, stream, false);
 | 
						|
    }
 | 
						|
 | 
						|
    zipW.close();
 | 
						|
 | 
						|
    return file;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Properly serialize a function into eval-able code string.
 | 
						|
   *
 | 
						|
   * @param {function} script
 | 
						|
   * @returns {string}
 | 
						|
   */
 | 
						|
  static serializeFunction(script) {
 | 
						|
    // Serialization of object methods doesn't include `function` anymore.
 | 
						|
    const method = /^(async )?(?:(\w+)|"(\w+)\.js")\(/;
 | 
						|
 | 
						|
    let code = script.toString();
 | 
						|
    let match = code.match(method);
 | 
						|
    if (match && match[2] !== "function") {
 | 
						|
      code = code.replace(method, "$1function $2$3(");
 | 
						|
    }
 | 
						|
    return code;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Properly serialize a script into eval-able code string.
 | 
						|
   *
 | 
						|
   * @param {string|function|Array} script
 | 
						|
   * @returns {string}
 | 
						|
   */
 | 
						|
  static serializeScript(script) {
 | 
						|
    if (Array.isArray(script)) {
 | 
						|
      return Array.from(script, this.serializeScript, this).join(";");
 | 
						|
    }
 | 
						|
    if (typeof script !== "function") {
 | 
						|
      return script;
 | 
						|
    }
 | 
						|
    return `(${this.serializeFunction(script)})();`;
 | 
						|
  }
 | 
						|
 | 
						|
  static setIncognitoOverride(extension) {
 | 
						|
    let { id, addonData } = extension;
 | 
						|
    if (!addonData || !addonData.incognitoOverride) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (addonData.incognitoOverride == "not_allowed") {
 | 
						|
      return ExtensionPermissions.remove(id, {
 | 
						|
        permissions: ["internal:privateBrowsingAllowed"],
 | 
						|
        origins: [],
 | 
						|
      });
 | 
						|
    }
 | 
						|
    return ExtensionPermissions.add(id, {
 | 
						|
      permissions: ["internal:privateBrowsingAllowed"],
 | 
						|
      origins: [],
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  static setExtensionID(data) {
 | 
						|
    try {
 | 
						|
      if (data.manifest.applications.gecko.id) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    } catch (e) {
 | 
						|
      // No ID is set.
 | 
						|
    }
 | 
						|
    provide(
 | 
						|
      data,
 | 
						|
      ["manifest", "applications", "gecko", "id"],
 | 
						|
      uuidGen.generateUUID().number
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Generates a new extension using |Extension.generateXPI|, and initializes a
 | 
						|
   * new |Extension| instance which will execute it.
 | 
						|
   *
 | 
						|
   * @param {object} data
 | 
						|
   * @returns {Extension}
 | 
						|
   */
 | 
						|
  static generate(data) {
 | 
						|
    let file = this.generateXPI(data);
 | 
						|
 | 
						|
    flushJarCache(file.path);
 | 
						|
    Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {
 | 
						|
      path: file.path,
 | 
						|
    });
 | 
						|
 | 
						|
    let fileURI = Services.io.newFileURI(file);
 | 
						|
    let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/");
 | 
						|
 | 
						|
    // This may be "temporary" or "permanent".
 | 
						|
    if (data.useAddonManager) {
 | 
						|
      return new MockExtension(file, jarURI, data);
 | 
						|
    }
 | 
						|
 | 
						|
    let id;
 | 
						|
    if (data.manifest) {
 | 
						|
      if (data.manifest.applications && data.manifest.applications.gecko) {
 | 
						|
        id = data.manifest.applications.gecko.id;
 | 
						|
      } else if (
 | 
						|
        data.manifest.browser_specific_settings &&
 | 
						|
        data.manifest.browser_specific_settings.gecko
 | 
						|
      ) {
 | 
						|
        id = data.manifest.browser_specific_settings.gecko.id;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (!id) {
 | 
						|
      id = uuidGen.generateUUID().number;
 | 
						|
    }
 | 
						|
 | 
						|
    let signedState = AddonManager.SIGNEDSTATE_SIGNED;
 | 
						|
    if (data.isPrivileged) {
 | 
						|
      signedState = AddonManager.SIGNEDSTATE_PRIVILEGED;
 | 
						|
    }
 | 
						|
    if (data.isSystem) {
 | 
						|
      signedState = AddonManager.SIGNEDSTATE_SYSTEM;
 | 
						|
    }
 | 
						|
 | 
						|
    return new Extension({
 | 
						|
      id,
 | 
						|
      resourceURI: jarURI,
 | 
						|
      cleanupFile: file,
 | 
						|
      signedState,
 | 
						|
      incognitoOverride: data.incognitoOverride,
 | 
						|
      temporarilyInstalled: !!data.temporarilyInstalled,
 | 
						|
      TEST_NO_ADDON_MANAGER: true,
 | 
						|
    });
 | 
						|
  }
 | 
						|
};
 |