summaryrefslogtreecommitdiffstats
path: root/toolkit/actors/RemotePageChild.sys.mjs
blob: b7cf7b20e9d965f1e8cc275faaf84caf7f229f22 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
/* 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/. */

/**
 * RemotePageChild is a base class for an unprivileged internal page, typically
 * an about: page. A specific implementation should subclass the RemotePageChild
 * actor with a more specific actor for that page. Typically, the child is not
 * needed, but the parent actor will respond to messages and provide results
 * directly to the page.
 */

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  AsyncPrefs: "resource://gre/modules/AsyncPrefs.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
  RemotePageAccessManager:
    "resource://gre/modules/RemotePageAccessManager.sys.mjs",
});

export class RemotePageChild extends JSWindowActorChild {
  actorCreated() {
    this.listeners = new Map();
    this.exportBaseFunctions();
  }

  exportBaseFunctions() {
    const exportableFunctions = [
      "RPMSendAsyncMessage",
      "RPMSendQuery",
      "RPMAddMessageListener",
      "RPMRemoveMessageListener",
      "RPMGetIntPref",
      "RPMGetStringPref",
      "RPMGetBoolPref",
      "RPMSetPref",
      "RPMGetFormatURLPref",
      "RPMIsWindowPrivate",
    ];

    this.exportFunctions(exportableFunctions);
  }

  /**
   * Exports a list of functions to be accessible by the privileged page.
   * Subclasses may call this function to add functions that are specific
   * to a page. When the page calls a function, a function with the same
   * name is called within the child actor.
   *
   * Only functions that appear in the whitelist in the
   * RemotePageAccessManager for that page will be exported.
   *
   * @param array of function names.
   */
  exportFunctions(functions) {
    let document = this.document;
    let principal = document.nodePrincipal;

    // If there is no content principal, don't export any functions.
    if (!principal) {
      return;
    }

    let window = this.contentWindow;

    for (let fnname of functions) {
      let allowAccess = lazy.RemotePageAccessManager.checkAllowAccessToFeature(
        principal,
        fnname,
        document
      );

      if (allowAccess) {
        // Wrap each function in an access checking function.
        function accessCheckedFn(...args) {
          this.checkAllowAccess(fnname, args[0]);
          return this[fnname](...args);
        }

        Cu.exportFunction(accessCheckedFn.bind(this), window, {
          defineAs: fnname,
        });
      }
    }
  }

  handleEvent() {
    // Do nothing. The DOMDocElementInserted event is just used to create
    // the actor.
  }

  receiveMessage(messagedata) {
    let message = {
      name: messagedata.name,
      data: messagedata.data,
    };

    let listeners = this.listeners.get(message.name);
    if (!listeners) {
      return;
    }

    let clonedMessage = Cu.cloneInto(message, this.contentWindow);
    for (let listener of listeners.values()) {
      try {
        listener(clonedMessage);
      } catch (e) {
        console.error(e);
      }
    }
  }

  wrapPromise(promise) {
    return new this.contentWindow.Promise((resolve, reject) =>
      promise.then(resolve, reject)
    );
  }

  /**
   * Returns true if a feature cannot be accessed by the current page.
   * Throws an exception if the feature may not be accessed.

   * @param aDocument child process document to call from
   * @param aFeature to feature to check access to
   * @param aValue value that must be included with that feature's whitelist
   * @returns true if access is allowed or throws an exception otherwise
   */
  checkAllowAccess(aFeature, aValue) {
    let doc = this.document;
    if (!lazy.RemotePageAccessManager.checkAllowAccess(doc, aFeature, aValue)) {
      throw new Error(
        "RemotePageAccessManager does not allow access to " + aFeature
      );
    }

    return true;
  }

  addPage(aUrl, aFunctionMap) {
    lazy.RemotePageAccessManager.addPage(aUrl, aFunctionMap);
  }

  // Implementation of functions that are exported into the page.

  RPMSendAsyncMessage(aName, aData = null) {
    this.sendAsyncMessage(aName, aData);
  }

  RPMSendQuery(aName, aData = null) {
    return this.wrapPromise(
      new Promise(resolve => {
        this.sendQuery(aName, aData).then(result => {
          resolve(Cu.cloneInto(result, this.contentWindow));
        });
      })
    );
  }

  /**
   * Adds a listener for messages. Many callbacks can be registered for the
   * same message if necessary. An attempt to register the same callback for the
   * same message twice will be ignored. When called the callback is passed an
   * object with these properties:
   *   name:   The message name
   *   data:   Any data sent with the message
   */
  RPMAddMessageListener(aName, aCallback) {
    if (!this.listeners.has(aName)) {
      this.listeners.set(aName, new Set([aCallback]));
    } else {
      this.listeners.get(aName).add(aCallback);
    }
  }

  /**
   * Removes a listener for messages.
   */
  RPMRemoveMessageListener(aName, aCallback) {
    if (!this.listeners.has(aName)) {
      return;
    }

    this.listeners.get(aName).delete(aCallback);
  }

  RPMGetIntPref(aPref, defaultValue) {
    // Only call with a default value if it's defined, to be able to throw
    // errors for non-existent prefs.
    if (defaultValue !== undefined) {
      return Services.prefs.getIntPref(aPref, defaultValue);
    }
    return Services.prefs.getIntPref(aPref);
  }

  RPMGetStringPref(aPref) {
    return Services.prefs.getStringPref(aPref);
  }

  RPMGetBoolPref(aPref, defaultValue) {
    // Only call with a default value if it's defined, to be able to throw
    // errors for non-existent prefs.
    if (defaultValue !== undefined) {
      return Services.prefs.getBoolPref(aPref, defaultValue);
    }
    return Services.prefs.getBoolPref(aPref);
  }

  RPMSetPref(aPref, aVal) {
    return this.wrapPromise(lazy.AsyncPrefs.set(aPref, aVal));
  }

  RPMGetFormatURLPref(aFormatURL) {
    return Services.urlFormatter.formatURLPref(aFormatURL);
  }

  RPMIsWindowPrivate() {
    return lazy.PrivateBrowsingUtils.isContentWindowPrivate(this.contentWindow);
  }
}