forked from mirrors/gecko-dev
		
	 fcedebb912
			
		
	
	
		fcedebb912
		
	
	
	
	
		
			
			MozReview-Commit-ID: Fqlv5BRuuW8 --HG-- extra : rebase_source : 348f037abd9cecfa080183bc365e5f005eac1bd6 extra : amend_source : 05dbfd12f553fc3f2a93374402e34d271e26d767
		
			
				
	
	
		
			424 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			424 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"];
 | |
| 
 | |
| ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| 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, "FileUtils",
 | |
|                                "resource://gre/modules/FileUtils.jsm");
 | |
| ChromeUtils.defineModuleGetter(this, "OS",
 | |
|                                "resource://gre/modules/osfile.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyGetter(this, "apiManager",
 | |
|                             () => ExtensionParent.apiManager);
 | |
| 
 | |
| ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 | |
| 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());
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * 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
 | |
|  * @param {boolean} [embedded = false]
 | |
|  */
 | |
| class MockExtension {
 | |
|   constructor(file, rootURI, installType, embedded) {
 | |
|     this.id = null;
 | |
|     this.file = file;
 | |
|     this.rootURI = rootURI;
 | |
|     this.installType = installType;
 | |
|     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");
 | |
|     if (!embedded) {
 | |
|       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);
 | |
|   }
 | |
| 
 | |
|   on(...args) {
 | |
|     this._extensionPromise.then(extension => {
 | |
|       extension.on(...args);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   off(...args) {
 | |
|     this._extensionPromise.then(extension => {
 | |
|       extension.off(...args);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   startup() {
 | |
|     if (this.installType == "temporary") {
 | |
|       return AddonManager.installTemporaryAddon(this.file).then(addon => {
 | |
|         this.addon = addon;
 | |
|         return this._readyPromise;
 | |
|       });
 | |
|     } else if (this.installType == "permanent") {
 | |
|       this.addonPromise = new Promise(resolve => {
 | |
|         this.resolveAddon = resolve;
 | |
|       });
 | |
|       return new Promise(async (resolve, reject) => {
 | |
|         let install = await AddonManager.getInstallForFile(this.file);
 | |
|         let listener = {
 | |
|           onInstallFailed: reject,
 | |
|           onInstallEnded: (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);
 | |
|   }
 | |
| }
 | |
| 
 | |
| var ExtensionTestCommon = class ExtensionTestCommon {
 | |
|   static generateManifest(manifest) {
 | |
|     provide(manifest, ["name"], "Generated extension");
 | |
|     provide(manifest, ["manifest_version"], 2);
 | |
|     provide(manifest, ["version"], "1.0");
 | |
|     return manifest;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * This code is designed to make it easy to test a WebExtension
 | |
|    * without creating a bunch of files. Everything is contained in a
 | |
|    * single JSON blob.
 | |
|    *
 | |
|    * 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.
 | |
|    *
 | |
|    * The generated extension is stored in the system temporary directory,
 | |
|    * and an nsIFile object pointing to it is returned.
 | |
|    *
 | |
|    * @param {object} data
 | |
|    * @returns {nsIFile}
 | |
|    */
 | |
|   static generateXPI(data) {
 | |
|     let manifest = data.manifest;
 | |
|     if (!manifest) {
 | |
|       manifest = {};
 | |
|     }
 | |
| 
 | |
|     let files = Object.assign({}, data.files);
 | |
| 
 | |
|     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"], manifest);
 | |
| 
 | |
|     if (data.embedded) {
 | |
|       // Package this as a webextension embedded inside a legacy
 | |
|       // extension.
 | |
| 
 | |
|       let xpiFiles = {
 | |
|         "install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
 | |
|           <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 | |
|                xmlns:em="http://www.mozilla.org/2004/em-rdf#">
 | |
|               <Description about="urn:mozilla:install-manifest"
 | |
|                   em:id="${manifest.applications.gecko.id}"
 | |
|                   em:name="${manifest.name}"
 | |
|                   em:type="2"
 | |
|                   em:version="${manifest.version}"
 | |
|                   em:description=""
 | |
|                   em:multiprocessCompatible="true"
 | |
|                   em:hasEmbeddedWebExtension="true"
 | |
|                   em:bootstrap="true">
 | |
| 
 | |
|                   <!-- Firefox -->
 | |
|                   <em:targetApplication>
 | |
|                       <Description
 | |
|                           em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
 | |
|                           em:minVersion="51.0a1"
 | |
|                           em:maxVersion="*"/>
 | |
|                   </em:targetApplication>
 | |
|                   <em:targetApplication>
 | |
|                     <Description>
 | |
|                       <em:id>toolkit@mozilla.org</em:id>
 | |
|                       <em:minVersion>0</em:minVersion>
 | |
|                       <em:maxVersion>*</em:maxVersion>
 | |
|                     </Description>
 | |
|                   </em:targetApplication>
 | |
|               </Description>
 | |
|           </RDF>
 | |
|         `,
 | |
| 
 | |
|         "bootstrap.js": `
 | |
|           function install() {}
 | |
|           function uninstall() {}
 | |
|           function shutdown() {}
 | |
| 
 | |
|           function startup(data) {
 | |
|             data.webExtension.startup();
 | |
|           }
 | |
|         `,
 | |
|       };
 | |
| 
 | |
|       for (let [path, data] of Object.entries(files)) {
 | |
|         xpiFiles[`webextension/${path}`] = data;
 | |
|       }
 | |
| 
 | |
|       files = xpiFiles;
 | |
|     }
 | |
| 
 | |
|     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 (typeof(script) == "function") {
 | |
|         script = this.serializeScript(script);
 | |
|       } else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
 | |
|         script = JSON.stringify(script);
 | |
|       }
 | |
| 
 | |
|       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)})();`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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.useAddonManager, data.embedded);
 | |
|     }
 | |
| 
 | |
|     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,
 | |
|       temporarilyInstalled: !!data.temporarilyInstalled,
 | |
|       TEST_NO_ADDON_MANAGER: true,
 | |
|     });
 | |
|   }
 | |
| };
 |