/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; const { Schemas } = ChromeUtils.importESModule( "resource://gre/modules/Schemas.sys.mjs" ); let { SchemaAPIInterface } = ExtensionCommon; const global = this; let json = [ { namespace: "revokableNs", permissions: ["revokableNs"], properties: { stringProp: { type: "string", writable: true, }, revokableStringProp: { type: "string", permissions: ["revokableProp"], writable: true, }, submoduleProp: { $ref: "submodule", }, revokableSubmoduleProp: { $ref: "submodule", permissions: ["revokableProp"], }, }, types: [ { id: "submodule", type: "object", functions: [ { name: "sub_foo", type: "function", parameters: [], returns: { type: "integer" }, }, ], }, ], functions: [ { name: "func", type: "function", parameters: [], }, { name: "revokableFunc", type: "function", parameters: [], permissions: ["revokableFunc"], }, ], events: [ { name: "onEvent", type: "function", }, { name: "onRevokableEvent", type: "function", permissions: ["revokableEvent"], }, ], }, ]; let recorded = []; function record(...args) { recorded.push(args); } function verify(expected) { for (let [i, rec] of expected.entries()) { Assert.deepEqual(recorded[i], rec, `Record ${i} matches`); } equal(recorded.length, expected.length, "Got expected number of records"); recorded.length = 0; } registerCleanupFunction(() => { equal(recorded.length, 0, "No unchecked recorded events at shutdown"); }); let permissions = new Set(); class APIImplementation extends SchemaAPIInterface { constructor(namespace, name) { super(); this.namespace = namespace; this.name = name; } record(method, args) { record(method, this.namespace, this.name, args); } revoke(...args) { this.record("revoke", args); } callFunction(...args) { this.record("callFunction", args); if (this.name === "sub_foo") { return 13; } } callFunctionNoReturn(...args) { this.record("callFunctionNoReturn", args); } getProperty(...args) { this.record("getProperty", args); } setProperty(...args) { this.record("setProperty", args); } addListener(...args) { this.record("addListener", args); } removeListener(...args) { this.record("removeListener", args); } hasListener(...args) { this.record("hasListener", args); } } let context = { manifestVersion: 2, cloneScope: global, permissionsChanged: null, setPermissionsChangedCallback(callback) { this.permissionsChanged = callback; }, hasPermission(permission) { return permissions.has(permission); }, isPermissionRevokable(permission) { return permission.startsWith("revokable"); }, getImplementation(namespace, name) { return new APIImplementation(namespace, name); }, shouldInject() { return true; }, }; function ignoreError(fn) { try { fn(); } catch (e) { // Meh. } } add_task(async function () { let url = "data:," + JSON.stringify(json); await Schemas.load(url); let root = {}; Schemas.inject(root, context); equal(recorded.length, 0, "No recorded events"); let listener = () => {}; let captured = {}; function checkRecorded() { let possible = [ ["revokableNs", ["getProperty", "revokableNs", "stringProp", []]], [ "revokableProp", ["getProperty", "revokableNs", "revokableStringProp", []], ], [ "revokableNs", ["setProperty", "revokableNs", "stringProp", ["stringProp"]], ], [ "revokableProp", [ "setProperty", "revokableNs", "revokableStringProp", ["revokableStringProp"], ], ], ["revokableNs", ["callFunctionNoReturn", "revokableNs", "func", [[]]]], [ "revokableFunc", ["callFunctionNoReturn", "revokableNs", "revokableFunc", [[]]], ], [ "revokableNs", ["callFunction", "revokableNs.submoduleProp", "sub_foo", [[]]], ], [ "revokableProp", ["callFunction", "revokableNs.revokableSubmoduleProp", "sub_foo", [[]]], ], [ "revokableNs", ["addListener", "revokableNs", "onEvent", [listener, []]], ], ["revokableNs", ["removeListener", "revokableNs", "onEvent", [listener]]], ["revokableNs", ["hasListener", "revokableNs", "onEvent", [listener]]], [ "revokableEvent", ["addListener", "revokableNs", "onRevokableEvent", [listener, []]], ], [ "revokableEvent", ["removeListener", "revokableNs", "onRevokableEvent", [listener]], ], [ "revokableEvent", ["hasListener", "revokableNs", "onRevokableEvent", [listener]], ], ]; let expected = []; if (permissions.has("revokableNs")) { for (let [perm, recording] of possible) { if (!perm || permissions.has(perm)) { expected.push(recording); } } } verify(expected); } function check() { info(`Check normal access (permissions: [${Array.from(permissions)}])`); let ns = root.revokableNs; void ns.stringProp; void ns.revokableStringProp; ns.stringProp = "stringProp"; ns.revokableStringProp = "revokableStringProp"; ns.func(); if (ns.revokableFunc) { ns.revokableFunc(); } ns.submoduleProp.sub_foo(); if (ns.revokableSubmoduleProp) { ns.revokableSubmoduleProp.sub_foo(); } ns.onEvent.addListener(listener); ns.onEvent.removeListener(listener); ns.onEvent.hasListener(listener); if (ns.onRevokableEvent) { ns.onRevokableEvent.addListener(listener); ns.onRevokableEvent.removeListener(listener); ns.onRevokableEvent.hasListener(listener); } checkRecorded(); } function capture() { info("Capture values"); let ns = root.revokableNs; captured = { ns }; captured.revokableStringProp = Object.getOwnPropertyDescriptor( ns, "revokableStringProp" ); captured.revokableSubmoduleProp = ns.revokableSubmoduleProp; if (ns.revokableSubmoduleProp) { captured.sub_foo = ns.revokableSubmoduleProp.sub_foo; } captured.revokableFunc = ns.revokableFunc; captured.onRevokableEvent = ns.onRevokableEvent; if (ns.onRevokableEvent) { captured.addListener = ns.onRevokableEvent.addListener; captured.removeListener = ns.onRevokableEvent.removeListener; captured.hasListener = ns.onRevokableEvent.hasListener; } } function checkCaptured() { info( `Check captured value access (permissions: [${Array.from(permissions)}])` ); let { ns } = captured; void ns.stringProp; ignoreError(() => captured.revokableStringProp.get()); if (!permissions.has("revokableProp")) { void ns.revokableStringProp; } ns.stringProp = "stringProp"; ignoreError(() => captured.revokableStringProp.set("revokableStringProp")); if (!permissions.has("revokableProp")) { ns.revokableStringProp = "revokableStringProp"; } ignoreError(() => ns.func()); ignoreError(() => captured.revokableFunc()); if (!permissions.has("revokableFunc")) { ignoreError(() => ns.revokableFunc()); } ignoreError(() => ns.submoduleProp.sub_foo()); ignoreError(() => captured.sub_foo()); if (!permissions.has("revokableProp")) { ignoreError(() => captured.revokableSubmoduleProp.sub_foo()); ignoreError(() => ns.revokableSubmoduleProp.sub_foo()); } ignoreError(() => ns.onEvent.addListener(listener)); ignoreError(() => ns.onEvent.removeListener(listener)); ignoreError(() => ns.onEvent.hasListener(listener)); ignoreError(() => captured.addListener(listener)); ignoreError(() => captured.removeListener(listener)); ignoreError(() => captured.hasListener(listener)); if (!permissions.has("revokableEvent")) { ignoreError(() => captured.onRevokableEvent.addListener(listener)); ignoreError(() => captured.onRevokableEvent.removeListener(listener)); ignoreError(() => captured.onRevokableEvent.hasListener(listener)); ignoreError(() => ns.onRevokableEvent.addListener(listener)); ignoreError(() => ns.onRevokableEvent.removeListener(listener)); ignoreError(() => ns.onRevokableEvent.hasListener(listener)); } checkRecorded(); } permissions.add("revokableNs"); permissions.add("revokableProp"); permissions.add("revokableFunc"); permissions.add("revokableEvent"); check(); capture(); checkCaptured(); permissions.delete("revokableProp"); context.permissionsChanged(); verify([ ["revoke", "revokableNs", "revokableStringProp", []], ["revoke", "revokableNs.revokableSubmoduleProp", "sub_foo", []], ]); check(); checkCaptured(); permissions.delete("revokableFunc"); context.permissionsChanged(); verify([["revoke", "revokableNs", "revokableFunc", []]]); check(); checkCaptured(); permissions.delete("revokableEvent"); context.permissionsChanged(); verify([["revoke", "revokableNs", "onRevokableEvent", []]]); check(); checkCaptured(); permissions.delete("revokableNs"); context.permissionsChanged(); verify([ ["revoke", "revokableNs", "stringProp", []], ["revoke", "revokableNs", "func", []], ["revoke", "revokableNs.submoduleProp", "sub_foo", []], ["revoke", "revokableNs", "onEvent", []], ]); checkCaptured(); permissions.add("revokableNs"); permissions.add("revokableProp"); permissions.add("revokableFunc"); permissions.add("revokableEvent"); context.permissionsChanged(); check(); capture(); checkCaptured(); permissions.delete("revokableProp"); permissions.delete("revokableFunc"); permissions.delete("revokableEvent"); context.permissionsChanged(); verify([ ["revoke", "revokableNs", "revokableStringProp", []], ["revoke", "revokableNs", "revokableFunc", []], ["revoke", "revokableNs.revokableSubmoduleProp", "sub_foo", []], ["revoke", "revokableNs", "onRevokableEvent", []], ]); check(); checkCaptured(); permissions.add("revokableProp"); permissions.add("revokableFunc"); permissions.add("revokableEvent"); context.permissionsChanged(); check(); capture(); checkCaptured(); permissions.delete("revokableNs"); context.permissionsChanged(); verify([ ["revoke", "revokableNs", "stringProp", []], ["revoke", "revokableNs", "revokableStringProp", []], ["revoke", "revokableNs", "func", []], ["revoke", "revokableNs", "revokableFunc", []], ["revoke", "revokableNs.submoduleProp", "sub_foo", []], ["revoke", "revokableNs.revokableSubmoduleProp", "sub_foo", []], ["revoke", "revokableNs", "onEvent", []], ["revoke", "revokableNs", "onRevokableEvent", []], ]); equal(root.revokableNs, undefined, "Namespace is not defined"); checkCaptured(); }); add_task(async function test_neuter() { context.permissionsChanged = null; let root = {}; Schemas.inject(root, context); equal(recorded.length, 0, "No recorded events"); permissions.add("revokableNs"); permissions.add("revokableProp"); permissions.add("revokableFunc"); permissions.add("revokableEvent"); let ns = root.revokableNs; let { submoduleProp } = ns; let lazyGetter = Object.getOwnPropertyDescriptor(submoduleProp, "sub_foo"); permissions.delete("revokableNs"); context.permissionsChanged(); verify([]); equal(root.revokableNs, undefined, "Should have no revokableNs"); equal(ns.submoduleProp, undefined, "Should have no ns.submoduleProp"); equal(submoduleProp.sub_foo, undefined, "No sub_foo"); lazyGetter.get.call(submoduleProp); equal(submoduleProp.sub_foo, undefined, "No sub_foo"); });