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
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5"use strict";
6
7var EXPORTED_SYMBOLS = ["RemotePageChild"];
8
9/**
10 * RemotePageChild is a base class for an unprivileged internal page, typically
11 * an about: page. A specific implementation should subclass the RemotePageChild
12 * actor with a more specific actor for that page. Typically, the child is not
13 * needed, but the parent actor will respond to messages and provide results
14 * directly to the page.
15 */
16
17const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
18ChromeUtils.defineModuleGetter(
19  this,
20  "AsyncPrefs",
21  "resource://gre/modules/AsyncPrefs.jsm"
22);
23ChromeUtils.defineModuleGetter(
24  this,
25  "PrivateBrowsingUtils",
26  "resource://gre/modules/PrivateBrowsingUtils.jsm"
27);
28ChromeUtils.defineModuleGetter(
29  this,
30  "RemotePageAccessManager",
31  "resource://gre/modules/RemotePageAccessManager.jsm"
32);
33
34class RemotePageChild extends JSWindowActorChild {
35  actorCreated() {
36    this.listeners = new Map();
37    this.exportBaseFunctions();
38  }
39
40  exportBaseFunctions() {
41    const exportableFunctions = [
42      "RPMSendAsyncMessage",
43      "RPMSendQuery",
44      "RPMAddMessageListener",
45      "RPMRemoveMessageListener",
46      "RPMGetIntPref",
47      "RPMGetStringPref",
48      "RPMGetBoolPref",
49      "RPMSetBoolPref",
50      "RPMGetFormatURLPref",
51      "RPMIsWindowPrivate",
52    ];
53
54    this.exportFunctions(exportableFunctions);
55  }
56
57  /**
58   * Exports a list of functions to be accessible by the privileged page.
59   * Subclasses may call this function to add functions that are specific
60   * to a page. When the page calls a function, a function with the same
61   * name is called within the child actor.
62   *
63   * Only functions that appear in the whitelist in the
64   * RemotePageAccessManager for that page will be exported.
65   *
66   * @param array of function names.
67   */
68  exportFunctions(functions) {
69    let document = this.document;
70    let principal = document.nodePrincipal;
71
72    // If there is no content principal, don't export any functions.
73    if (!principal) {
74      return;
75    }
76
77    let window = this.contentWindow;
78
79    for (let fnname of functions) {
80      let allowAccess = RemotePageAccessManager.checkAllowAccessToFeature(
81        principal,
82        fnname,
83        document
84      );
85
86      if (allowAccess) {
87        // Wrap each function in an access checking function.
88        function accessCheckedFn(...args) {
89          this.checkAllowAccess(fnname, args[0]);
90          return this[fnname](...args);
91        }
92
93        Cu.exportFunction(accessCheckedFn.bind(this), window, {
94          defineAs: fnname,
95        });
96      }
97    }
98  }
99
100  handleEvent() {
101    // Do nothing. The DOMDocElementInserted event is just used to create
102    // the actor.
103  }
104
105  receiveMessage(messagedata) {
106    let message = {
107      name: messagedata.name,
108      data: messagedata.data,
109    };
110
111    let listeners = this.listeners.get(message.name);
112    if (!listeners) {
113      return;
114    }
115
116    let clonedMessage = Cu.cloneInto(message, this.contentWindow);
117    for (let listener of listeners.values()) {
118      try {
119        listener(clonedMessage);
120      } catch (e) {
121        Cu.reportError(e);
122      }
123    }
124  }
125
126  wrapPromise(promise) {
127    return new this.contentWindow.Promise((resolve, reject) =>
128      promise.then(resolve, reject)
129    );
130  }
131
132  /**
133   * Returns true if a feature cannot be accessed by the current page.
134   * Throws an exception if the feature may not be accessed.
135
136   * @param aDocument child process document to call from
137   * @param aFeature to feature to check access to
138   * @param aValue value that must be included with that feature's whitelist
139   * @returns true if access is allowed or throws an exception otherwise
140   */
141  checkAllowAccess(aFeature, aValue) {
142    let doc = this.document;
143    if (!RemotePageAccessManager.checkAllowAccess(doc, aFeature, aValue)) {
144      throw new Error(
145        "RemotePageAccessManager does not allow access to " + aFeature
146      );
147    }
148
149    return true;
150  }
151
152  // Implementation of functions that are exported into the page.
153
154  RPMSendAsyncMessage(aName, aData = null) {
155    this.sendAsyncMessage(aName, aData);
156  }
157
158  RPMSendQuery(aName, aData = null) {
159    return this.wrapPromise(
160      new Promise(resolve => {
161        this.sendQuery(aName, aData).then(result => {
162          resolve(Cu.cloneInto(result, this.contentWindow));
163        });
164      })
165    );
166  }
167
168  /**
169   * Adds a listener for messages. Many callbacks can be registered for the
170   * same message if necessary. An attempt to register the same callback for the
171   * same message twice will be ignored. When called the callback is passed an
172   * object with these properties:
173   *   name:   The message name
174   *   data:   Any data sent with the message
175   */
176  RPMAddMessageListener(aName, aCallback) {
177    if (!this.listeners.has(aName)) {
178      this.listeners.set(aName, new Set([aCallback]));
179    } else {
180      this.listeners.get(aName).add(aCallback);
181    }
182  }
183
184  /**
185   * Removes a listener for messages.
186   */
187  RPMRemoveMessageListener(aName, aCallback) {
188    if (!this.listeners.has(aName)) {
189      return;
190    }
191
192    this.listeners.get(aName).delete(aCallback);
193  }
194
195  RPMGetIntPref(aPref, defaultValue) {
196    // Only call with a default value if it's defined, to be able to throw
197    // errors for non-existent prefs.
198    if (defaultValue !== undefined) {
199      return Services.prefs.getIntPref(aPref, defaultValue);
200    }
201    return Services.prefs.getIntPref(aPref);
202  }
203
204  RPMGetStringPref(aPref) {
205    return Services.prefs.getStringPref(aPref);
206  }
207
208  RPMGetBoolPref(aPref, defaultValue) {
209    // Only call with a default value if it's defined, to be able to throw
210    // errors for non-existent prefs.
211    if (defaultValue !== undefined) {
212      return Services.prefs.getBoolPref(aPref, defaultValue);
213    }
214    return Services.prefs.getBoolPref(aPref);
215  }
216
217  RPMSetBoolPref(aPref, aVal) {
218    return this.wrapPromise(AsyncPrefs.set(aPref, aVal));
219  }
220
221  RPMGetFormatURLPref(aFormatURL) {
222    return Services.urlFormatter.formatURLPref(aFormatURL);
223  }
224
225  RPMIsWindowPrivate() {
226    return PrivateBrowsingUtils.isContentWindowPrivate(this.contentWindow);
227  }
228}
229