summaryrefslogtreecommitdiffstats
path: root/toolkit/components/webextensions/ExtensionManagement.jsm
blob: 324c5b71b933d0650c5604b0f8ce294fdf641806 (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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
/* 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.EXPORTED_SYMBOLS = ["ExtensionManagement"];

const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;

Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
                                  "resource://gre/modules/ExtensionUtils.jsm");

XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());

XPCOMUtils.defineLazyGetter(this, "UUIDMap", () => {
  let {UUIDMap} = Cu.import("resource://gre/modules/Extension.jsm", {});
  return UUIDMap;
});

/*
 * This file should be kept short and simple since it's loaded even
 * when no extensions are running.
 */

// Keep track of frame IDs for content windows. Mostly we can just use
// the outer window ID as the frame ID. However, the API specifies
// that top-level windows have a frame ID of 0. So we need to keep
// track of which windows are top-level. This code listens to messages
// from ExtensionContent to do that.
var Frames = {
  // Window IDs of top-level content windows.
  topWindowIds: new Set(),

  init() {
    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
      return;
    }

    Services.mm.addMessageListener("Extension:TopWindowID", this);
    Services.mm.addMessageListener("Extension:RemoveTopWindowID", this, true);
  },

  isTopWindowId(windowId) {
    return this.topWindowIds.has(windowId);
  },

  // Convert an outer window ID to a frame ID. An outer window ID of 0
  // is invalid.
  getId(windowId) {
    if (this.isTopWindowId(windowId)) {
      return 0;
    }
    if (windowId == 0) {
      return -1;
    }
    return windowId;
  },

  // Convert an outer window ID for a parent window to a frame
  // ID. Outer window IDs follow the same convention that
  // |window.top.parent === window.top|. The API works differently,
  // giving a frame ID of -1 for the the parent of a top-level
  // window. This function handles the conversion.
  getParentId(parentWindowId, windowId) {
    if (parentWindowId == windowId) {
      // We have a top-level window.
      return -1;
    }

    // Not a top-level window. Just return the ID as normal.
    return this.getId(parentWindowId);
  },

  receiveMessage({name, data}) {
    switch (name) {
      case "Extension:TopWindowID":
        // FIXME: Need to handle the case where the content process
        // crashes. Right now we leak its top window IDs.
        this.topWindowIds.add(data.windowId);
        break;

      case "Extension:RemoveTopWindowID":
        this.topWindowIds.delete(data.windowId);
        break;
    }
  },
};
Frames.init();

var APIs = {
  apis: new Map(),

  register(namespace, schema, script) {
    if (this.apis.has(namespace)) {
      throw new Error(`API namespace already exists: ${namespace}`);
    }

    this.apis.set(namespace, {schema, script});
  },

  unregister(namespace) {
    if (!this.apis.has(namespace)) {
      throw new Error(`API namespace does not exist: ${namespace}`);
    }

    this.apis.delete(namespace);
  },
};

function getURLForExtension(id, path = "") {
  let uuid = UUIDMap.get(id, false);
  if (!uuid) {
    Cu.reportError(`Called getURLForExtension on unmapped extension ${id}`);
    return null;
  }
  return `moz-extension://${uuid}/${path}`;
}

// This object manages various platform-level issues related to
// moz-extension:// URIs. It lives here so that it can be used in both
// the parent and child processes.
//
// moz-extension URIs have the form moz-extension://uuid/path. Each
// extension has its own UUID, unique to the machine it's installed
// on. This is easier and more secure than using the extension ID,
// since it makes it slightly harder to fingerprint for extensions if
// each user uses different URIs for the extension.
var Service = {
  initialized: false,

  // Map[uuid -> extension].
  // extension can be an Extension (parent process) or BrowserExtensionContent (child process).
  uuidMap: new Map(),

  init() {
    let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService);
    aps = aps.wrappedJSObject;
    this.aps = aps;
    aps.setExtensionURILoadCallback(this.extensionURILoadableByAnyone.bind(this));
    aps.setExtensionURIToAddonIdCallback(this.extensionURIToAddonID.bind(this));
  },

  // Called when a new extension is loaded.
  startupExtension(uuid, uri, extension) {
    if (!this.initialized) {
      this.initialized = true;
      this.init();
    }

    // Create the moz-extension://uuid mapping.
    let handler = Services.io.getProtocolHandler("moz-extension");
    handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
    handler.setSubstitution(uuid, uri);

    this.uuidMap.set(uuid, extension);
    this.aps.setAddonHasPermissionCallback(extension.id, extension.hasPermission.bind(extension));
    this.aps.setAddonLoadURICallback(extension.id, this.checkAddonMayLoad.bind(this, extension));
    this.aps.setAddonLocalizeCallback(extension.id, extension.localize.bind(extension));
    this.aps.setAddonCSP(extension.id, extension.manifest.content_security_policy);
    this.aps.setBackgroundPageUrlCallback(uuid, this.generateBackgroundPageUrl.bind(this, extension));
  },

  // Called when an extension is unloaded.
  shutdownExtension(uuid) {
    let extension = this.uuidMap.get(uuid);
    this.uuidMap.delete(uuid);
    this.aps.setAddonHasPermissionCallback(extension.id, null);
    this.aps.setAddonLoadURICallback(extension.id, null);
    this.aps.setAddonLocalizeCallback(extension.id, null);
    this.aps.setAddonCSP(extension.id, null);
    this.aps.setBackgroundPageUrlCallback(uuid, null);

    let handler = Services.io.getProtocolHandler("moz-extension");
    handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
    handler.setSubstitution(uuid, null);
  },

  // Return true if the given URI can be loaded from arbitrary web
  // content. The manifest.json |web_accessible_resources| directive
  // determines this.
  extensionURILoadableByAnyone(uri) {
    let uuid = uri.host;
    let extension = this.uuidMap.get(uuid);
    if (!extension || !extension.webAccessibleResources) {
      return false;
    }

    let path = uri.QueryInterface(Ci.nsIURL).filePath;
    if (path.length > 0 && path[0] == "/") {
      path = path.substr(1);
    }
    return extension.webAccessibleResources.matches(path);
  },

  // Checks whether a given extension can load this URI (typically via
  // an XML HTTP request). The manifest.json |permissions| directive
  // determines this.
  checkAddonMayLoad(extension, uri) {
    return extension.whiteListedHosts.matchesIgnoringPath(uri);
  },

  generateBackgroundPageUrl(extension) {
    let background_scripts = extension.manifest.background &&
      extension.manifest.background.scripts;
    if (!background_scripts) {
      return;
    }
    let html = "<!DOCTYPE html>\n<body>\n";
    for (let script of background_scripts) {
      script = script.replace(/"/g, "&quot;");
      html += `<script src="${script}"></script>\n`;
    }
    html += "</body>\n</html>\n";
    return "data:text/html;charset=utf-8," + encodeURIComponent(html);
  },

  // Finds the add-on ID associated with a given moz-extension:// URI.
  // This is used to set the addonId on the originAttributes for the
  // nsIPrincipal attached to the URI.
  extensionURIToAddonID(uri) {
    let uuid = uri.host;
    let extension = this.uuidMap.get(uuid);
    return extension ? extension.id : undefined;
  },
};

// API Levels Helpers

// Find the add-on associated with this document via the
// principal's originAttributes. This value is computed by
// extensionURIToAddonID, which ensures that we don't inject our
// API into webAccessibleResources or remote web pages.
function getAddonIdForWindow(window) {
  return Cu.getObjectPrincipal(window).originAttributes.addonId;
}

const API_LEVELS = Object.freeze({
  NO_PRIVILEGES: 0,
  CONTENTSCRIPT_PRIVILEGES: 1,
  FULL_PRIVILEGES: 2,
});

// Finds the API Level ("FULL_PRIVILEGES", "CONTENTSCRIPT_PRIVILEGES", "NO_PRIVILEGES")
// with a given a window object.
function getAPILevelForWindow(window, addonId) {
  const {NO_PRIVILEGES, CONTENTSCRIPT_PRIVILEGES, FULL_PRIVILEGES} = API_LEVELS;

  // Non WebExtension URLs and WebExtension URLs from a different extension
  // has no access to APIs.
  if (!addonId || getAddonIdForWindow(window) != addonId) {
    return NO_PRIVILEGES;
  }

  // Extension pages running in the content process always defaults to
  // "content script API level privileges".
  if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
    return CONTENTSCRIPT_PRIVILEGES;
  }

  let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIDocShell);

  // Handling of ExtensionPages running inside sub-frames.
  if (docShell.sameTypeParent) {
    let parentWindow = docShell.sameTypeParent.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIDOMWindow);

    // The option page iframe embedded in the about:addons tab should have
    // full API level privileges. (see Bug 1256282 for rationale)
    let parentDocument = parentWindow.document;
    let parentIsSystemPrincipal = Services.scriptSecurityManager
                                          .isSystemPrincipal(parentDocument.nodePrincipal);
    if (parentDocument.location.href == "about:addons" && parentIsSystemPrincipal) {
      return FULL_PRIVILEGES;
    }

    // The addon iframes embedded in a addon page from with the same addonId
    // should have the same privileges of the sameTypeParent.
    // (see Bug 1258347 for rationale)
    let parentSameAddonPrivileges = getAPILevelForWindow(parentWindow, addonId);
    if (parentSameAddonPrivileges > NO_PRIVILEGES) {
      return parentSameAddonPrivileges;
    }

    // In all the other cases, WebExtension URLs loaded into sub-frame UI
    // will have "content script API level privileges".
    // (see Bug 1214658 for rationale)
    return CONTENTSCRIPT_PRIVILEGES;
  }

  // WebExtension URLs loaded into top frames UI could have full API level privileges.
  return FULL_PRIVILEGES;
}

this.ExtensionManagement = {
  startupExtension: Service.startupExtension.bind(Service),
  shutdownExtension: Service.shutdownExtension.bind(Service),

  registerAPI: APIs.register.bind(APIs),
  unregisterAPI: APIs.unregister.bind(APIs),

  getFrameId: Frames.getId.bind(Frames),
  getParentFrameId: Frames.getParentId.bind(Frames),

  getURLForExtension,

  // exported API Level Helpers
  getAddonIdForWindow,
  getAPILevelForWindow,
  API_LEVELS,

  APIs,
};