DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (d38398e5144e)

VCS Links

Line Code
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 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
/* 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, "E10SUtils",
                                  "resource:///modules/E10SUtils.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;
});

var ExtensionManagement;

/*
 * 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, explicit = false) {
    return extension.whiteListedHosts.matchesIgnoringPath(uri, explicit);
  },

  generateBackgroundPageUrl(extension) {
    let background_scripts = (extension.manifest.background &&
                              extension.manifest.background.scripts);

    if (!background_scripts) {
      return;
    }

    let html = "<!DOCTYPE html>\n<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;
  }

  if (!ExtensionManagement.isExtensionProcess) {
    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;
    }

    // NOTE: Special handling for devtools panels using a chrome iframe here
    // for the devtools panel, it is needed because a content iframe breaks
    // switching between docked and undocked mode (see bug 1075490).
    let devtoolsBrowser = parentDocument.querySelector(
      "browser[webextension-view-type='devtools_panel']");
    if (devtoolsBrowser && devtoolsBrowser.contentWindow === window &&
        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;
}

ExtensionManagement = {
  get isExtensionProcess() {
    if (this.useRemoteWebExtensions) {
      return Services.appinfo.remoteType === E10SUtils.EXTENSION_REMOTE_TYPE;
    }
    return Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
  },

  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,
};

XPCOMUtils.defineLazyPreferenceGetter(ExtensionManagement, "useRemoteWebExtensions",
                                      "extensions.webextensions.remote", false);