Bug 1733104 - Implement runtime.getFrameId available from content scripts r=robwu
[gecko.git] / toolkit / components / extensions / child / ext-runtime.js
blob3ba9db2b7aeb0e7275559123dff8bd749b239aa2
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
6 ChromeUtils.defineModuleGetter(
7   this,
8   "WebNavigationFrames",
9   "resource://gre/modules/WebNavigationFrames.jsm"
12 /**
13  * With optional arguments on both ends, this case is ambiguous:
14  *     runtime.sendMessage("string", {} or nullish)
15  *
16  * Sending a message within the extension is more common than sending
17  * an empty object to another extension, so we prefer that conclusion.
18  *
19  * @param {string?}  [extensionId]
20  * @param {any}      message
21  * @param {object?}  [options]
22  * @param {function} [callback]
23  * @returns {{extensionId: string?, message: any, callback: function?}}
24  */
25 function parseBonkersArgs(...args) {
26   let Error = ExtensionUtils.ExtensionError;
27   let callback = typeof args[args.length - 1] === "function" && args.pop();
29   // We don't support any options anymore, so only an empty object is valid.
30   function validOptions(v) {
31     return v == null || (typeof v === "object" && !Object.keys(v).length);
32   }
34   if (args.length === 1 || (args.length === 2 && validOptions(args[1]))) {
35     // Interpret as passing null for extensionId (message within extension).
36     args.unshift(null);
37   }
38   let [extensionId, message, options] = args;
40   if (!args.length) {
41     throw new Error("runtime.sendMessage's message argument is missing");
42   } else if (!validOptions(options)) {
43     throw new Error("runtime.sendMessage's options argument is invalid");
44   } else if (args.length === 4 && args[3] && !callback) {
45     throw new Error("runtime.sendMessage's last argument is not a function");
46   } else if (args[3] != null || args.length > 4) {
47     throw new Error("runtime.sendMessage received too many arguments");
48   } else if (extensionId && typeof extensionId !== "string") {
49     throw new Error("runtime.sendMessage's extensionId argument is invalid");
50   }
51   return { extensionId, message, callback };
54 this.runtime = class extends ExtensionAPI {
55   getAPI(context) {
56     let { extension } = context;
58     return {
59       runtime: {
60         onConnect: context.messenger.onConnect.api(),
61         onMessage: context.messenger.onMessage.api(),
63         onConnectExternal: context.messenger.onConnectEx.api(),
64         onMessageExternal: context.messenger.onMessageEx.api(),
66         connect(extensionId, options) {
67           let name = options?.name ?? "";
68           return context.messenger.connect({ name, extensionId });
69         },
71         sendMessage(...args) {
72           let arg = parseBonkersArgs(...args);
73           return context.messenger.sendRuntimeMessage(arg);
74         },
76         connectNative(name) {
77           return context.messenger.connect({ name, native: true });
78         },
80         sendNativeMessage(nativeApp, message) {
81           return context.messenger.sendNativeMessage(nativeApp, message);
82         },
84         get lastError() {
85           return context.lastError;
86         },
88         getManifest() {
89           return Cu.cloneInto(extension.manifest, context.cloneScope);
90         },
92         id: extension.id,
94         getURL(url) {
95           return extension.baseURI.resolve(url);
96         },
98         getFrameId(target) {
99           let frameId = WebNavigationFrames.getFromWindow(target);
100           if (frameId >= 0) {
101             return frameId;
102           }
103           // Not a WindowProxy, perhaps an embedder element?
105           let type;
106           try {
107             type = Cu.getClassName(target, true);
108           } catch (e) {
109             // Not a valid object, will throw below.
110           }
112           const embedderTypes = [
113             "HTMLIFrameElement",
114             "HTMLFrameElement",
115             "HTMLEmbedElement",
116             "HTMLObjectElement",
117           ];
119           if (embedderTypes.includes(type)) {
120             if (!target.browsingContext) {
121               return -1;
122             }
123             return WebNavigationFrames.getFrameId(target.browsingContext);
124           }
126           throw new ExtensionUtils.ExtensionError("Invalid argument");
127         },
128       },
129     };
130   }
132   getAPIObjectForRequest(context, request) {
133     if (request.apiObjectType === "Port") {
134       const port = context.messenger.getPortById(request.apiObjectId);
135       if (!port) {
136         throw new Error(`Port API object not found: ${request}`);
137       }
138       return port.api;
139     }
141     throw new Error(`Unexpected apiObjectType: ${request}`);
142   }