Bug 1171200 - Add means of checking if a document links to a manifest. r=billm
authorMarcos Caceres <marcos@marcosc.com>
Thu, 30 Jul 2015 11:56:12 -0400
changeset 287130 7692b5a9d0c57bc9f761daf7780117cdd5c2e0a8
parent 287129 82b165aa319217350751ed25dd9168bd3416b665
child 287131 3a1da6fff2ed1259e99853f8b9f671b36db53095
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1171200
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1171200 - Add means of checking if a document links to a manifest. r=billm
dom/ipc/manifestMessages.js
dom/manifest/ImageObjectProcessor.js
dom/manifest/ImageObjectProcessor.jsm
dom/manifest/ManifestFinder.jsm
dom/manifest/ManifestObtainer.js
dom/manifest/ManifestObtainer.jsm
dom/manifest/ManifestProcessor.js
dom/manifest/ManifestProcessor.jsm
dom/manifest/ValueExtractor.js
dom/manifest/ValueExtractor.jsm
dom/manifest/WebManifest.jsm
dom/manifest/moz.build
dom/manifest/test/browser.ini
dom/manifest/test/browser_ManifestFinder_browserHasManifestLink.js
dom/manifest/test/browser_ManifestObtainer_obtain.js
dom/manifest/test/common.js
dom/security/test/csp/browser_test_web_manifest.js
dom/security/test/csp/browser_test_web_manifest_mixed_content.js
toolkit/modules/PromiseMessage.jsm
toolkit/modules/moz.build
--- a/dom/ipc/manifestMessages.js
+++ b/dom/ipc/manifestMessages.js
@@ -6,117 +6,86 @@
  * http://www.w3.org/TR/appmanifest/#obtaining
  *
  * It searches a top-level browsing context for
  * a <link rel=manifest> element. Then fetches
  * and processes the linked manifest.
  *
  * BUG: https://bugzilla.mozilla.org/show_bug.cgi?id=1083410
  */
-/*globals content, sendAsyncMessage, addMessageListener, Components*/
-'use strict';
+/*globals Task, ManifestObtainer, ManifestFinder, content, sendAsyncMessage, addMessageListener, Components*/
+"use strict";
 const {
   utils: Cu,
-  classes: Cc,
-  interfaces: Ci
 } = Components;
-const {
-  ManifestProcessor
-} = Cu.import('resource://gre/modules/WebManifest.jsm', {});
-const {
-  Task: {
-    spawn, async
-  }
-} = Components.utils.import('resource://gre/modules/Task.jsm', {});
+Cu.import("resource://gre/modules/ManifestObtainer.jsm");
+Cu.import("resource://gre/modules/ManifestFinder.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+const MessageHandler = {
+  registerListeners() {
+    addMessageListener(
+      "DOM:WebManifest:hasManifestLink",
+      this.hasManifestLink.bind(this)
+    );
+    addMessageListener(
+      "DOM:ManifestObtainer:Obtain",
+      this.obtainManifest.bind(this)
+    );
+  },
+
+  /**
+   * Check if the content document includes a link to a web manifest.
+   * @param {Object} aMsg The IPC message, which is destructured to just
+   *                      get the id.
+   */
+  hasManifestLink({data: {id}}) {
+    const response = makeMsgResponse(id);
+    response.result = ManifestFinder.contentHasManifestLink(content);
+    response.success = true;
+    sendAsyncMessage("DOM:WebManifest:hasManifestLink", response);
+  },
 
-addMessageListener('DOM:ManifestObtainer:Obtain', async(function* (aMsg) {
-  const response = {
-    msgId: aMsg.data.msgId,
-    success: true,
-    result: undefined
-  };
-  try {
-    response.result = yield fetchManifest();
-  } catch (err) {
-    response.success = false;
-    response.result = cloneError(err);
-  }
-  sendAsyncMessage('DOM:ManifestObtainer:Obtain', response);
-}));
-
-function cloneError(aError) {
+  /**
+   * Asynchronously obtains a web manifest from content by using the
+   * ManifestObtainer and messages back the result.
+   * @param {Object} aMsg The IPC message, which is destructured to just
+   *                      get the id.
+   */
+  obtainManifest: Task.async(function* ({data: {id}}) {
+    const response = makeMsgResponse(id);
+    try {
+      response.result = yield ManifestObtainer.contentObtainManifest(content);
+      response.success = true;
+    } catch (err) {
+      response.result = serializeError(err);
+    }
+    sendAsyncMessage("DOM:ManifestObtainer:Obtain", response);
+  }),
+};
+/**
+ * Utility function to Serializes an JS Error, so it can be transferred over
+ * the message channel.
+ * FIX ME: https://bugzilla.mozilla.org/show_bug.cgi?id=1172586
+ * @param  {Error} aError The error to serialize.
+ * @return {Object} The serialized object.
+ */
+function serializeError(aError) {
   const clone = {
-    'fileName': String(aError.fileName),
-    'lineNumber': String(aError.lineNumber),
-    'columnNumber': String(aError.columnNumber),
-    'stack': String(aError.stack),
-    'message': String(aError.message),
-    'name': String(aError.name)
+    "fileName": aError.fileName,
+    "lineNumber": aError.lineNumber,
+    "columnNumber": aError.columnNumber,
+    "stack": aError.stack,
+    "message": aError.message,
+    "name": aError.name
   };
   return clone;
 }
 
-function fetchManifest() {
-  return spawn(function* () {
-    if (!content || content.top !== content) {
-      let msg = 'Content window must be a top-level browsing context.';
-      throw new Error(msg);
-    }
-    const elem = content.document.querySelector('link[rel~="manifest"]');
-    if (!elem || !elem.getAttribute('href')) {
-      let msg = 'No manifest to fetch.';
-      throw new Error(msg);
-    }
-    // Throws on malformed URLs
-    const manifestURL = new content.URL(elem.href, elem.baseURI);
-    if (!canLoadManifest(elem)) {
-      let msg = `Content Security Policy: The page's settings blocked the `;
-      msg += `loading of a resource at ${elem.href}`;
-      throw new Error(msg);
-    }
-    const reqInit = {
-      mode: 'cors'
+function makeMsgResponse(aId) {
+    return {
+      id: aId,
+      success: false,
+      result: undefined
     };
-    if (elem.crossOrigin === 'use-credentials') {
-      reqInit.credentials = 'include';
-    }
-    const req = new content.Request(manifestURL, reqInit);
-    req.setContentPolicyType(Ci.nsIContentPolicy.TYPE_WEB_MANIFEST);
-    const response = yield content.fetch(req);
-    const manifest = yield processResponse(response, content);
-    return manifest;
-  });
-}
+  }
 
-function canLoadManifest(aElem) {
-  const contentPolicy = Cc['@mozilla.org/layout/content-policy;1']
-    .getService(Ci.nsIContentPolicy);
-  const mimeType = aElem.type || 'application/manifest+json';
-  const elemURI = BrowserUtils.makeURI(
-    aElem.href, aElem.ownerDocument.characterSet
-  );
-  const shouldLoad = contentPolicy.shouldLoad(
-    Ci.nsIContentPolicy.TYPE_WEB_MANIFEST, elemURI,
-    aElem.ownerDocument.documentURIObject,
-    aElem, mimeType, null
-  );
-  return shouldLoad === Ci.nsIContentPolicy.ACCEPT;
-}
-
-function processResponse(aResp, aContentWindow) {
-  return spawn(function* () {
-    const badStatus = aResp.status < 200 || aResp.status >= 300;
-    if (aResp.type === 'error' || badStatus) {
-      let msg =
-        `Fetch error: ${aResp.status} - ${aResp.statusText} at ${aResp.url}`;
-      throw new Error(msg);
-    }
-    const text = yield aResp.text();
-    const args = {
-      jsonText: text,
-      manifestURL: aResp.url,
-      docURL: aContentWindow.location.href
-    };
-    const processor = new ManifestProcessor();
-    const manifest = processor.process(args);
-    return Cu.cloneInto(manifest, content);
-  });
-}
+MessageHandler.registerListeners();
rename from dom/manifest/ImageObjectProcessor.js
rename to dom/manifest/ImageObjectProcessor.jsm
new file mode 100644
--- /dev/null
+++ b/dom/manifest/ManifestFinder.jsm
@@ -0,0 +1,68 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+/* globals Components, Task, PromiseMessage */
+"use strict";
+const {
+  utils: Cu
+} = Components;
+Cu.import("resource://gre/modules/PromiseMessage.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+this.ManifestFinder = {// jshint ignore:line
+  /**
+  * Check from content process if DOM Window has a conforming
+  * manifest link relationship.
+  * @param aContent DOM Window to check.
+  * @return {Promise<Boolean>}
+  */
+  contentHasManifestLink(aContent) {
+    if (!aContent || isXULBrowser(aContent)) {
+      throw new TypeError("Invalid input.");
+    }
+    return checkForManifest(aContent);
+  },
+
+  /**
+  * Check from a XUL browser (parent process) if it's content document has a
+  * manifest link relationship.
+  * @param aBrowser The XUL browser to check.
+  * @return {Promise}
+  */
+  browserHasManifestLink: Task.async(
+    function* (aBrowser) {
+      if (!isXULBrowser(aBrowser)) {
+        throw new TypeError("Invalid input.");
+      }
+      const msgKey = "DOM:WebManifest:hasManifestLink";
+      const mm = aBrowser.messageManager;
+      const reply = yield PromiseMessage.send(mm, msgKey);
+      return reply.data.result;
+    }
+  )
+};
+
+function isXULBrowser(aBrowser) {
+  if (!aBrowser || !aBrowser.namespaceURI || !aBrowser.localName) {
+    return false;
+  }
+  const XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+  return (aBrowser.namespaceURI === XUL && aBrowser.localName === "browser");
+}
+
+function checkForManifest(aWindow) {
+  // Only top-level browsing contexts are valid.
+  if (!aWindow || aWindow.top !== aWindow) {
+    return false;
+  }
+  const elem = aWindow.document.querySelector("link[rel~='manifest']");
+  // Only if we have an element and a non-empty href attribute.
+  if (!elem || !elem.getAttribute("href")) {
+    return false;
+  }
+  return true;
+}
+
+this.EXPORTED_SYMBOLS = [// jshint ignore:line
+  "ManifestFinder"
+];
deleted file mode 100644
--- a/dom/manifest/ManifestObtainer.js
+++ /dev/null
@@ -1,92 +0,0 @@
-/* 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/.
- *
- * ManifestObtainer is an implementation of:
- * http://w3c.github.io/manifest/#obtaining
- *
- * Exposes public method `.obtainManifest(browserWindow)`, which returns
- * a promise. If successful, you get back a manifest (string).
- *
- * For e10s compat, this JSM relies on the following to do
- * the nessesary IPC:
- *   dom/ipc/manifestMessages.js
- *
- * whose internal URL is:
- *   'chrome://global/content/manifestMessages.js'
- *
- * Which is injected into every browser instance via browser.js.
- *
- * BUG: https://bugzilla.mozilla.org/show_bug.cgi?id=1083410
- * exported ManifestObtainer
- */
-'use strict';
-const MSG_KEY = 'DOM:ManifestObtainer:Obtain';
-let messageCounter = 0;
-// FIXME: Ideally, we would store a reference to the
-//        message manager in a weakmap instead of needing a
-//        browserMap. However, trying to store a messageManager
-//        results in a TypeError because of:
-//        https://bugzilla.mozilla.org/show_bug.cgi?id=888600
-const browsersMap = new WeakMap();
-
-function ManifestObtainer() {}
-
-ManifestObtainer.prototype = {
-  obtainManifest(aBrowserWindow) {
-    if (!aBrowserWindow) {
-      const err = new TypeError('Invalid input. Expected xul browser.');
-      return Promise.reject(err);
-    }
-    const mm = aBrowserWindow.messageManager;
-    const onMessage = function(aMsg) {
-      const msgId = aMsg.data.msgId;
-      const {
-        resolve, reject
-      } = browsersMap.get(aBrowserWindow).get(msgId);
-      browsersMap.get(aBrowserWindow).delete(msgId);
-      // If we we've processed all messages,
-      // stop listening.
-      if (!browsersMap.get(aBrowserWindow).size) {
-        browsersMap.delete(aBrowserWindow);
-        mm.removeMessageListener(MSG_KEY, onMessage);
-      }
-      if (aMsg.data.success) {
-        return resolve(aMsg.data.result);
-      }
-      reject(toError(aMsg.data.result));
-    };
-    // If we are not already listening for messages
-    // start listening.
-    if (!browsersMap.has(aBrowserWindow)) {
-      browsersMap.set(aBrowserWindow, new Map());
-      mm.addMessageListener(MSG_KEY, onMessage);
-    }
-    return new Promise((resolve, reject) => {
-      const msgId = messageCounter++;
-      browsersMap.get(aBrowserWindow).set(msgId, {
-        resolve: resolve,
-        reject: reject
-      });
-      mm.sendAsyncMessage(MSG_KEY, {
-        msgId: msgId
-      });
-    });
-
-    function toError(aErrorClone) {
-      let error;
-      switch (aErrorClone.name) {
-      case 'TypeError':
-        error = new TypeError();
-        break;
-      default:
-        error = new Error();
-      }
-      Object.getOwnPropertyNames(aErrorClone)
-        .forEach(name => error[name] = aErrorClone[name]);
-      return error;
-    }
-  }
-};
-this.ManifestObtainer = ManifestObtainer; // jshint ignore:line
-this.EXPORTED_SYMBOLS = ['ManifestObtainer']; // jshint ignore:line
new file mode 100644
--- /dev/null
+++ b/dom/manifest/ManifestObtainer.jsm
@@ -0,0 +1,175 @@
+/* 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/.
+ */
+ /*
+ * ManifestObtainer is an implementation of:
+ * http://w3c.github.io/manifest/#obtaining
+ *
+ * Exposes 2 public method:
+ *
+ *  .contentObtainManifest(aContent) - used in content process
+ *  .browserObtainManifest(aBrowser) - used in browser/parent process
+ *
+ * both return a promise. If successful, you get back a manifest object.
+ *
+ * Import it with URL:
+ *   'chrome://global/content/manifestMessages.js'
+ *
+ * e10s IPC message from this components are handled by:
+ *   dom/ipc/manifestMessages.js
+ *
+ * Which is injected into every browser instance via browser.js.
+ *
+ * exported ManifestObtainer
+ */
+/*globals Components, Task, PromiseMessage, XPCOMUtils, ManifestProcessor, BrowserUtils*/
+"use strict";
+const {
+  utils: Cu,
+  classes: Cc,
+  interfaces: Ci
+} = Components;
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/PromiseMessage.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/ManifestProcessor.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",  // jshint ignore:line
+  "resource://gre/modules/BrowserUtils.jsm");
+
+this.ManifestObtainer = { // jshint ignore:line
+  /**
+  * Public interface for obtaining a web manifest from a XUL browser, to use
+  * on the parent process.
+  * @param  {XULBrowser} The browser to check for the manifest.
+  * @return {Promise<Object>} The processed manifest.
+  */
+  browserObtainManifest: Task.async(function* (aBrowser) {
+    const msgKey = "DOM:ManifestObtainer:Obtain";
+    if (!isXULBrowser(aBrowser)) {
+      throw new TypeError("Invalid input. Expected XUL browser.");
+    }
+    const mm = aBrowser.messageManager;
+    const {data: {success, result}} = yield PromiseMessage.send(mm, msgKey);
+    if (!success) {
+      const error = toError(result);
+      throw error;
+    }
+    return result;
+  }),
+  /**
+   * Public interface for obtaining a web manifest from a XUL browser.
+   * @param  {Window} The content Window from which to extract the manifest.
+   * @return {Promise<Object>} The processed manifest.
+   */
+  contentObtainManifest: Task.async(function* (aContent) {
+    if (!aContent || isXULBrowser(aContent)) {
+      throw new TypeError("Invalid input. Expected a DOM Window.");
+    }
+    const manifest = yield fetchManifest(aContent);
+    return manifest;
+  }
+)};
+
+function toError(aErrorClone) {
+  let error;
+  switch (aErrorClone.name) {
+  case "TypeError":
+    error = new TypeError();
+    break;
+  default:
+    error = new Error();
+  }
+  Object.getOwnPropertyNames(aErrorClone)
+    .forEach(name => error[name] = aErrorClone[name]);
+  return error;
+}
+
+function isXULBrowser(aBrowser) {
+  if (!aBrowser || !aBrowser.namespaceURI || !aBrowser.localName) {
+    return false;
+  }
+  const XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+  return (aBrowser.namespaceURI === XUL && aBrowser.localName === "browser");
+}
+
+/**
+ * Asynchronously processes the result of response after having fetched
+ * a manifest.
+ * @param {Response} aResp Response from fetch().
+ * @param {Window} aContentWindow The content window.
+ * @return {Promise<Object>} The processed manifest.
+ */
+const processResponse = Task.async(function* (aResp, aContentWindow) {
+  const badStatus = aResp.status < 200 || aResp.status >= 300;
+  if (aResp.type === "error" || badStatus) {
+    const msg =
+      `Fetch error: ${aResp.status} - ${aResp.statusText} at ${aResp.url}`;
+    throw new Error(msg);
+  }
+  const text = yield aResp.text();
+  const args = {
+    jsonText: text,
+    manifestURL: aResp.url,
+    docURL: aContentWindow.location.href
+  };
+  const manifest = ManifestProcessor.process(args);
+  return manifest;
+});
+
+/**
+ * Asynchronously fetches a web manifest.
+ * @param {Window} a The content Window from where to extract the manifest.
+ * @return {Promise<Object>}
+ */
+const fetchManifest = Task.async(function* (aWindow) {
+  if (!aWindow || aWindow.top !== aWindow) {
+    let msg = "Window must be a top-level browsing context.";
+    throw new Error(msg);
+  }
+  const elem = aWindow.document.querySelector("link[rel~='manifest']");
+  if (!elem || !elem.getAttribute("href")) {
+    let msg = `No manifest to fetch at ${aWindow.location}`;
+    throw new Error(msg);
+  }
+  // Throws on malformed URLs
+  const manifestURL = new aWindow.URL(elem.href, elem.baseURI);
+  if (!canLoadManifest(elem)) {
+    let msg = `Content Security Policy: The page's settings blocked the `;
+    msg += `loading of a resource at ${elem.href}`;
+    throw new Error(msg);
+  }
+  const reqInit = {
+    mode: "cors"
+  };
+  if (elem.crossOrigin === "use-credentials") {
+    reqInit.credentials = "include";
+  }
+  const req = new aWindow.Request(manifestURL, reqInit);
+  req.setContentPolicyType(Ci.nsIContentPolicy.TYPE_WEB_MANIFEST);
+  const response = yield aWindow.fetch(req);
+  const manifest = yield processResponse(response, aWindow);
+  return manifest;
+});
+
+/**
+ * Checks against security manager if we can load the web manifest.
+ * @param  {HTMLLinkElement} aElem The HTML element to security check.
+ * @return {Boolean} True if it can, false if it can't.
+ */
+function canLoadManifest(aElem) {
+  const contentPolicy = Cc["@mozilla.org/layout/content-policy;1"]
+    .getService(Ci.nsIContentPolicy);
+  const mimeType = aElem.type || "application/manifest+json";
+  const elemURI = BrowserUtils.makeURI(
+    aElem.href, aElem.ownerDocument.characterSet
+  );
+  const shouldLoad = contentPolicy.shouldLoad(
+    Ci.nsIContentPolicy.TYPE_WEB_MANIFEST, elemURI,
+    aElem.ownerDocument.documentURIObject,
+    aElem, mimeType, null
+  );
+  return shouldLoad === Ci.nsIContentPolicy.ACCEPT;
+}
+
+this.EXPORTED_SYMBOLS = ["ManifestObtainer"]; // jshint ignore:line
rename from dom/manifest/ManifestProcessor.js
rename to dom/manifest/ManifestProcessor.jsm
--- a/dom/manifest/ManifestProcessor.js
+++ b/dom/manifest/ManifestProcessor.jsm
@@ -14,66 +14,46 @@
  *
  * Depends on ImageObjectProcessor to process things like
  * icons and splash_screens.
  *
  * TODO: The constructor should accept the UA's supported orientations.
  * TODO: The constructor should accept the UA's supported display modes.
  * TODO: hook up developer tools to console. (1086997).
  */
-/*globals Components*/
+/*globals Components, ValueExtractor, ImageObjectProcessor, ConsoleAPI*/
 'use strict';
 const {
-  utils: Cu,
-  interfaces: Ci,
-  classes: Cc
+  utils: Cu
 } = Components;
 Cu.importGlobalProperties(['URL']);
 const displayModes = new Set(['fullscreen', 'standalone', 'minimal-ui',
   'browser'
 ]);
 const orientationTypes = new Set(['any', 'natural', 'landscape', 'portrait',
   'portrait-primary', 'portrait-secondary', 'landscape-primary',
   'landscape-secondary'
 ]);
-const {
-  ConsoleAPI
-} = Cu.import('resource://gre/modules/devtools/Console.jsm', {});
+Cu.import('resource://gre/modules/devtools/Console.jsm');
 // ValueExtractor is used by the various processors to get values
 // from the manifest and to report errors.
-const {
-  ValueExtractor
-} = Cu.import('resource://gre/modules/ValueExtractor.js', {});
+Cu.import('resource://gre/modules/ValueExtractor.jsm');
 // ImageObjectProcessor is used to process things like icons and images
-const {
-  ImageObjectProcessor
-} = Cu.import('resource://gre/modules/ImageObjectProcessor.js', {});
-
-function ManifestProcessor() {}
+Cu.import('resource://gre/modules/ImageObjectProcessor.jsm');
 
-// Static getters
-Object.defineProperties(ManifestProcessor, {
-  'defaultDisplayMode': {
-    get: function() {
-      return 'browser';
-    }
+this.ManifestProcessor = { // jshint ignore:line
+  get defaultDisplayMode() {
+    return 'browser';
   },
-  'displayModes': {
-    get: function() {
-      return displayModes;
-    }
+  get displayModes() {
+    return displayModes;
   },
-  'orientationTypes': {
-    get: function() {
-      return orientationTypes;
-    }
-  }
-});
-
-ManifestProcessor.prototype = {
+  get orientationTypes() {
+    return orientationTypes;
+  },
   // process() method processes JSON text into a clean manifest
   // that conforms with the W3C specification. Takes an object
   // expecting the following dictionary items:
   //  * jsonText: the JSON string to be processed.
   //  * manifestURL: the URL of the manifest, to resolve URLs.
   //  * docURL: the URL of the owner doc, for security checks
   process({
     jsonText,
@@ -94,18 +74,18 @@ ManifestProcessor.prototype = {
       console.warn(msg);
       rawManifest = {};
     }
     const extractor = new ValueExtractor(console);
     const imgObjProcessor = new ImageObjectProcessor(console, extractor);
     const processedManifest = {
       'lang': processLangMember(),
       'start_url': processStartURLMember(),
-      'display': processDisplayMember(),
-      'orientation': processOrientationMember(),
+      'display': processDisplayMember.call(this),
+      'orientation': processOrientationMember.call(this),
       'name': processNameMember(),
       'icons': imgObjProcessor.process(
         rawManifest, manifestURL, 'icons'
       ),
       'splash_screens': imgObjProcessor.process(
         rawManifest, manifestURL, 'splash_screens'
       ),
       'short_name': processShortNameMember(),
@@ -140,36 +120,36 @@ ManifestProcessor.prototype = {
       const spec = {
         objectName: 'manifest',
         object: rawManifest,
         property: 'orientation',
         expectedType: 'string',
         trim: true
       };
       const value = extractor.extractValue(spec);
-      if (ManifestProcessor.orientationTypes.has(value)) {
+      if (this.orientationTypes.has(value)) {
         return value;
       }
       // The spec special-cases orientation to return the empty string.
       return '';
     }
 
     function processDisplayMember() {
       const spec = {
         objectName: 'manifest',
         object: rawManifest,
         property: 'display',
         expectedType: 'string',
         trim: true
       };
       const value = extractor.extractValue(spec);
-      if (ManifestProcessor.displayModes.has(value)) {
+      if (displayModes.has(value)) {
         return value;
       }
-      return ManifestProcessor.defaultDisplayMode;
+      return this.defaultDisplayMode;
     }
 
     function processScopeMember() {
       const spec = {
         objectName: 'manifest',
         object: rawManifest,
         property: 'scope',
         expectedType: 'string',
@@ -244,26 +224,24 @@ ManifestProcessor.prototype = {
       return extractor.extractColorValue(spec);
     }
 
     function processLangMember() {
       const spec = {
         objectName: 'manifest',
         object: rawManifest,
         property: 'lang',
-        expectedType: 'string',
-        trim: true
+        expectedType: 'string', trim: true
       };
       let tag = extractor.extractValue(spec);
       // TODO: Check if tag is structurally valid.
       //       Cannot do this because we don't support Intl API on Android.
       //       https://bugzilla.mozilla.org/show_bug.cgi?id=864843
       //       https://github.com/tc39/ecma402/issues/5
       // TODO: perform canonicalization on the tag.
       //       Can't do this today because there is no direct means to
       //       access canonicalization algorithms through Intl API.
       //       https://github.com/tc39/ecma402/issues/5
       return tag;
     }
   }
 };
-this.ManifestProcessor = ManifestProcessor; // jshint ignore:line
 this.EXPORTED_SYMBOLS = ['ManifestProcessor']; // jshint ignore:line
rename from dom/manifest/ValueExtractor.js
rename to dom/manifest/ValueExtractor.jsm
--- a/dom/manifest/ValueExtractor.js
+++ b/dom/manifest/ValueExtractor.jsm
@@ -20,19 +20,17 @@ ValueExtractor.prototype = {
   // This function takes a 'spec' object and destructures
   // it to extract a value. If the value is of th wrong type, it
   // warns the developer and returns undefined.
   //  expectType: is the type of a JS primitive (string, number, etc.)
   //  object: is the object from which to extract the value.
   //  objectName: string used to construct the developer warning.
   //  property: the name of the property being extracted.
   //  trim: boolean, if the value should be trimmed (used by string type).
-  extractValue({
-      expectedType, object, objectName, property, trim
-    }) {
+  extractValue({expectedType, object, objectName, property, trim}) {
     const value = object[property];
     const isArray = Array.isArray(value);
     // We need to special-case "array", as it's not a JS primitive.
     const type = (isArray) ? 'array' : typeof value;
     if (type !== expectedType) {
       if (type !== 'undefined') {
         let msg = `Expected the ${objectName}'s ${property} `;
         msg += `member to be a ${expectedType}.`;
deleted file mode 100644
--- a/dom/manifest/WebManifest.jsm
+++ /dev/null
@@ -1,19 +0,0 @@
-/* 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 https://mozilla.org/MPL/2.0/. */
-/*exported EXPORTED_SYMBOLS, ManifestProcessor, ManifestObtainer*/
-/*globals Components */
-'use strict';
-const {
-  utils: Cu
-} = Components;
-
-this.EXPORTED_SYMBOLS = [
-  'ManifestObtainer',
-  'ManifestProcessor'
-];
-
-// Export public interfaces
-for (let symbl of EXPORTED_SYMBOLS) {
-  Cu.import(`resource://gre/modules/${symbl}.js`);
-}
--- a/dom/manifest/moz.build
+++ b/dom/manifest/moz.build
@@ -1,16 +1,16 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 EXTRA_JS_MODULES += [
-    'ImageObjectProcessor.js',
-    'ManifestObtainer.js',
-    'ManifestProcessor.js',
-    'ValueExtractor.js',
-    'WebManifest.jsm'
+    'ImageObjectProcessor.jsm',
+    'ManifestFinder.jsm',
+    'ManifestObtainer.jsm',
+    'ManifestProcessor.jsm',
+    'ValueExtractor.jsm',
 ]
 
 MOCHITEST_MANIFESTS += ['test/mochitest.ini']
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/dom/manifest/test/browser.ini
+++ b/dom/manifest/test/browser.ini
@@ -1,2 +1,3 @@
 [DEFAULT]
-[browser_ManifestObtainer_obtain.js]
\ No newline at end of file
+[browser_ManifestFinder_browserHasManifestLink.js]
+[browser_ManifestObtainer_obtain.js]
new file mode 100644
--- /dev/null
+++ b/dom/manifest/test/browser_ManifestFinder_browserHasManifestLink.js
@@ -0,0 +1,114 @@
+//Used by JSHint:
+/*global Cu, BrowserTestUtils, is, ok, add_task, gBrowser, ManifestFinder */
+"use strict";
+Cu.import("resource://gre/modules/ManifestFinder.jsm", this);  // jshint ignore:line
+
+const defaultURL =
+  "http://example.org/tests/dom/manifest/test/resource.sjs";
+const tests = [{
+  expected: "Document has a web manifest.",
+  get tabURL() {
+    let query = [
+      `body=<h1>${this.expected}</h1>`,
+      "Content-Type=text/html; charset=utf-8",
+    ];
+    const URL = `${defaultURL}?${query.join("&")}`;
+    return URL;
+  },
+  run(result) {
+    is(result, true, this.expected);
+  },
+  testData: `
+      <link rel="manifesto" href='${defaultURL}?body={"name":"fail"}'>
+      <link rel="foo bar manifest bar test" href='${defaultURL}?body={"name":"value"}'>
+      <link rel="manifest" href='${defaultURL}?body={"name":"fail"}'>`
+}, {
+  expected: "Document does not have a web manifest.",
+  get tabURL() {
+    let query = [
+      `body=<h1>${this.expected}</h1>`,
+      "Content-Type=text/html; charset=utf-8",
+    ];
+    const URL = `${defaultURL}?${query.join("&")}`;
+    return URL;
+  },
+  run(result) {
+    is(result, false, this.expected);
+  },
+  testData: `
+      <link rel="amanifista" href='${defaultURL}?body={"name":"fail"}'>
+      <link rel="foo bar manifesto bar test" href='${defaultURL}?body={"name":"pass-1"}'>
+      <link rel="manifesto" href='${defaultURL}?body={"name":"fail"}'>`
+}, {
+  expected: "Manifest link is has empty href.",
+  get tabURL() {
+    let query = [
+      `body=<h1>${this.expected}</h1>`,
+      "Content-Type=text/html; charset=utf-8",
+    ];
+    const URL = `${defaultURL}?${query.join("&")}`;
+    return URL;
+  },
+  run(result) {
+    is(result, false, this.expected);
+  },
+  testData: `
+  <link rel="manifest" href="">
+  <link rel="manifest" href='${defaultURL}?body={"name":"fail"}'>`
+}, {
+  expected: "Manifest link is missing.",
+  get tabURL() {
+    let query = [
+      `body=<h1>${this.expected}</h1>`,
+      "Content-Type=text/html; charset=utf-8",
+    ];
+    const URL = `${defaultURL}?${query.join("&")}`;
+    return URL;
+  },
+  run(result) {
+    is(result, false, this.expected);
+  },
+  testData: `
+    <link rel="manifest">
+    <link rel="manifest" href='${defaultURL}?body={"name":"fail"}'>`
+}];
+
+/**
+ * Test basic API error conditions
+ */
+add_task(function* () {
+  let expected = "Invalid types should throw a TypeError.";
+  for (let invalidValue of [undefined, null, 1, {}, "test"]) {
+    try {
+      yield ManifestFinder.contentManifestLink(invalidValue);
+      ok(false, expected);
+    } catch (e) {
+      is(e.name, "TypeError", expected);
+    }
+    try {
+      yield ManifestFinder.browserManifestLink(invalidValue);
+      ok(false, expected);
+    } catch (e) {
+      is(e.name, "TypeError", expected);
+    }
+  }
+});
+
+add_task(function* () {
+  for (let test of tests) {
+    let tabOptions = {
+      gBrowser: gBrowser,
+      url: test.tabURL,
+    };
+    yield BrowserTestUtils.withNewTab(
+      tabOptions,
+      browser => testHasManifest(browser, test)
+    );
+  }
+
+  function* testHasManifest(aBrowser, aTest) {
+    aBrowser.contentWindowAsCPOW.document.head.innerHTML = aTest.testData;
+    const result = yield ManifestFinder.browserHasManifestLink(aBrowser);
+    aTest.run(result);
+  }
+});
--- a/dom/manifest/test/browser_ManifestObtainer_obtain.js
+++ b/dom/manifest/test/browser_ManifestObtainer_obtain.js
@@ -1,14 +1,13 @@
 //Used by JSHint:
-/*global Cu, BrowserTestUtils, add_task, SpecialPowers, gBrowser, Assert*/
-'use strict';
+/*global requestLongerTimeout, Cu, BrowserTestUtils, add_task, SpecialPowers, gBrowser, Assert*/ 'use strict';
 const {
   ManifestObtainer
-} = Cu.import('resource://gre/modules/WebManifest.jsm', {});
+} = Cu.import('resource://gre/modules/ManifestObtainer.jsm', {});
 
 requestLongerTimeout(4); // e10s tests take time.
 const defaultURL =
   'http://example.org/tests/dom/manifest/test/resource.sjs';
 const remoteURL =
   'http://mochi.test:8888/tests/dom/manifest/test/resource.sjs';
 const tests = [
   // Fetch tests.
@@ -172,34 +171,32 @@ add_task(function*() {
     };
     yield BrowserTestUtils.withNewTab(
       tabOptions,
       browser => testObtainingManifest(browser, test)
     );
   }
 
   function* testObtainingManifest(aBrowser, aTest) {
-    const obtainer = new ManifestObtainer();
     aBrowser.contentWindowAsCPOW.document.head.innerHTML = aTest.testData;
     try {
-      const manifest = yield obtainer.obtainManifest(aBrowser);
+      const manifest = yield ManifestObtainer.browserObtainManifest(aBrowser);
       aTest.run(manifest);
     } catch (e) {
       aTest.run(e);
     }
   }
 });
 
 /*
  * e10s race condition tests
  * Open a bunch of tabs and load manifests
  * in each tab. They should all return pass.
  */
 add_task(function*() {
-  const obtainer = new ManifestObtainer();
   const defaultPath = '/tests/dom/manifest/test/manifestLoader.html';
   const tabURLs = [
     `http://test:80${defaultPath}`,
     `http://mochi.test:8888${defaultPath}`,
     `http://test1.mochi.test:8888${defaultPath}`,
     `http://sub1.test1.mochi.test:8888${defaultPath}`,
     `http://sub2.xn--lt-uia.mochi.test:8888${defaultPath}`,
     `http://test2.mochi.test:8888${defaultPath}`,
@@ -232,17 +229,17 @@ add_task(function*() {
   ];
   // Once all the pages have loaded, run a bunch of tests in "parallel".
   yield Promise.all((
     for (browser of browsers) BrowserTestUtils.browserLoaded(browser)
   ));
   // Flood random browsers with requests. Once promises settle, check that
   // responses all pass.
   const results = yield Promise.all((
-    for (browser of randBrowsers(browsers, 100)) obtainer.obtainManifest(browser)
+    for (browser of randBrowsers(browsers, 100)) ManifestObtainer.browserObtainManifest(browser)
   ));
   const expected = 'Expect every manifest to have name equal to `pass`.';
   const pass = results.every(manifest => manifest.name === 'pass');
   Assert.ok(pass, expected);
   //cleanup
   browsers
     .map(browser => gBrowser.getTabForBrowser(browser))
     .forEach(tab => gBrowser.removeTab(tab));
--- a/dom/manifest/test/common.js
+++ b/dom/manifest/test/common.js
@@ -1,17 +1,17 @@
 /**
  * Common infrastructure for manifest tests.
  **/
-
+/*globals SpecialPowers, ManifestProcessor*/
 'use strict';
 const {
   ManifestProcessor
-} = SpecialPowers.Cu.import('resource://gre/modules/WebManifest.jsm');
-const processor = new ManifestProcessor();
+} = SpecialPowers.Cu.import('resource://gre/modules/ManifestProcessor.jsm');
+const processor = ManifestProcessor;
 const manifestURL = new URL(document.location.origin + '/manifest.json');
 const docURL = document.location;
 const seperators = '\u2028\u2029\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000';
 const lineTerminators = '\u000D\u000A\u2028\u2029';
 const whiteSpace = `${seperators}${lineTerminators}`;
 const typeTests = [1, null, {},
   [], false
 ];
--- a/dom/security/test/csp/browser_test_web_manifest.js
+++ b/dom/security/test/csp/browser_test_web_manifest.js
@@ -5,17 +5,17 @@
  *   In particular, the tests check that default-src and manifest-src directives are
  *   are respected by the ManifestObtainer.
  */
 /*globals Components*/
 'use strict';
 requestLongerTimeout(10); // e10s tests take time.
 const {
   ManifestObtainer
-} = Components.utils.import('resource://gre/modules/WebManifest.jsm', {});
+} = Cu.import('resource://gre/modules/ManifestObtainer.jsm', {});
 const path = '/tests/dom/security/test/csp/';
 const testFile = `file=${path}file_web_manifest.html`;
 const remoteFile = `file=${path}file_web_manifest_remote.html`;
 const httpsManifest = `file=${path}file_web_manifest_https.html`;
 const mixedContent = `file=${path}file_web_manifest_mixed_content.html`;
 const server = 'file_testserver.sjs';
 const defaultURL = `http://example.org${path}${server}`;
 const remoteURL = `http://mochi.test:8888`;
@@ -215,22 +215,21 @@ add_task(function* () {
     yield BrowserTestUtils.withNewTab(
       tabOptions,
       browser => testObtainingManifest(browser, test)
     );
   }
 
   function* testObtainingManifest(aBrowser, aTest) {
     const observer = (/blocks/.test(aTest.expected)) ? new NetworkObserver(aTest) : null;
-    const obtainer = new ManifestObtainer();
     let manifest;
     // Expect an exception (from promise rejection) if there a content policy
     // that is violated.
     try {
-      manifest = yield obtainer.obtainManifest(aBrowser);
+      manifest = yield ManifestObtainer.browserObtainManifest(aBrowser);
     } catch (e) {
       const msg = `Expected promise rejection obtaining.`;
       ok(/blocked the loading of a resource/.test(e.message), msg);
       if (observer) {
         yield observer.finished;
       }
       return;
     }
--- a/dom/security/test/csp/browser_test_web_manifest_mixed_content.js
+++ b/dom/security/test/csp/browser_test_web_manifest_mixed_content.js
@@ -1,17 +1,17 @@
 /*
  * Description of the test:
  *   Check that mixed content blocker works prevents fetches of
  *   mixed content manifests.
  */
 'use strict';
 const {
   ManifestObtainer
-} = Components.utils.import('resource://gre/modules/WebManifest.jsm', {});
+} = Cu.import('resource://gre/modules/ManifestObtainer.jsm', {});
 const path = '/tests/dom/security/test/csp/';
 const mixedContent = `file=${path}file_web_manifest_mixed_content.html`;
 const server = 'file_testserver.sjs';
 const secureURL = `https://example.com${path}${server}`;
 const tests = [
   // Trying to load mixed content in file_web_manifest_mixed_content.html
   // needs to result in an error.
   {
@@ -40,16 +40,15 @@ add_task(function*() {
     };
     yield BrowserTestUtils.withNewTab(
       tabOptions,
       browser => testObtainingManifest(browser, test)
     );
   }
 
   function* testObtainingManifest(aBrowser, aTest) {
-    const obtainer = new ManifestObtainer();
     try {
-      yield obtainer.obtainManifest(aBrowser);
+      yield ManifestObtainer.browserObtainManifest(aBrowser);
     } catch (e) {
       aTest.run(e)
     }
   }
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/PromiseMessage.jsm
@@ -0,0 +1,36 @@
+/* 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 = ["PromiseMessage"];
+
+let msgId = 0;
+
+let PromiseMessage = {
+  send(messageManager, name, data = {}) {
+    let id = msgId++;
+
+    // Make a copy of data so that the caller doesn't see us setting 'id'.
+    let dataCopy = {};
+    for (let prop in data) {
+      dataCopy[prop] = data[prop];
+    }
+    dataCopy.id = id;
+
+    // Send the message.
+    messageManager.sendAsyncMessage(name, dataCopy);
+
+    // Return a promise that resolves when we get a reply (a message of the same name).
+    return new Promise(resolve => {
+      messageManager.addMessageListener(name, function listener(reply) {
+        if (reply.data.id !== id) {
+          return;
+        }
+        messageManager.removeMessageListener(name, listener);
+        resolve(reply);
+      });
+    });
+  }
+};
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -43,16 +43,17 @@ EXTRA_JS_MODULES += [
     'PageMetadata.jsm',
     'PermissionsUtils.jsm',
     'PopupNotifications.jsm',
     'Preferences.jsm',
     'PrivateBrowsingUtils.jsm',
     'ProfileAge.jsm',
     'Promise-backend.js',
     'Promise.jsm',
+    'PromiseMessage.jsm',
     'PromiseUtils.jsm',
     'PropertyListUtils.jsm',
     'RemoteController.jsm',
     'RemoteFinder.jsm',
     'RemotePageManager.jsm',
     'RemoteSecurityUI.jsm',
     'RemoteWebNavigation.jsm',
     'RemoteWebProgress.jsm',