Bug 1171200 - Add means of checking if a document links to a manifest. r=billm.
authorMarcos Caceres <marcos@marcosc.com>
Wed, 08 Jul 2015 13:26:32 +1000
changeset 251850 01d03b6be047ec5263030bc104780137cca92425
parent 251849 cec27384d57d5f9bc5c25f177547c0b11b3e43b3
child 251851 97b92a4f10abfa1f31b27162778f9202d2a862e6
push id29013
push usercbook@mozilla.com
push dateWed, 08 Jul 2015 09:47:46 +0000
treeherdermozilla-central@9b902b7669ae [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1171200, 100644
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 | 166 +++++++++----------- ...ObjectProcessor.js => ImageObjectProcessor.jsm} | 0 dom/manifest/ManifestFinder.jsm | 58 +++++++ dom/manifest/ManifestObtainer.js | 92 ----------- dom/manifest/ManifestObtainer.jsm | 170 +++++++++++++++++++++ ...{ManifestProcessor.js => ManifestProcessor.jsm} | 18 +-- .../{ValueExtractor.js => ValueExtractor.jsm} | 4 +- dom/manifest/WebManifest.jsm | 19 --- dom/manifest/moz.build | 10 +- dom/manifest/test/browser.ini | 3 +- .../test/browser_ManifestObtainer_obtain.js | 2 +- dom/manifest/test/browser_hasManifestLink.js | 109 +++++++++++++ dom/manifest/test/common.js | 4 +- dom/security/test/csp/browser_test_web_manifest.js | 12 +- .../csp/browser_test_web_manifest_mixed_content.js | 10 +- toolkit/modules/PromiseMessage.jsm | 36 +++++ toolkit/modules/moz.build | 1 + 17 files changed, 467 insertions(+), 247 deletions(-) rename dom/manifest/{ImageObjectProcessor.js => ImageObjectProcessor.jsm} (100%) create mode 100644 dom/manifest/ManifestFinder.jsm delete mode 100644 dom/manifest/ManifestObtainer.js create mode 100644 dom/manifest/ManifestObtainer.jsm rename dom/manifest/{ManifestProcessor.js => ManifestProcessor.jsm} (95%) rename dom/manifest/{ValueExtractor.js => ValueExtractor.jsm} (96%) delete mode 100644 dom/manifest/WebManifest.jsm create mode 100644 dom/manifest/test/browser_hasManifestLink.js create mode 100644 toolkit/modules/PromiseMessage.jsm
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_ManifestObtainer_obtain.js
dom/manifest/test/browser_hasManifestLink.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,87 @@
  * 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*/
+/*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 finder = new ManifestFinder();
+
+const MessageHandler = {
+  registerListeners() {
+    addMessageListener(
+      'DOM:WebManifest:hasManifestLink',
+      this.hasManifestLink.bind(this)
+    );
+    addMessageListener(
+      'DOM:ManifestObtainer:Obtain',
+      this.obtainManifest.bind(this)
+    );
+  },
 
-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);
-}));
+  /**
+   * Check if the content document includes a link to a web manifest.
+   * @param {Object} aMsg The IPC message.
+   */
+  hasManifestLink: Task.async(function* ({data: {id}}) {
+    const response = this.makeMsgResponse(id);
+    response.result = yield finder.hasManifestLink(content);
+    response.success = true;
+    sendAsyncMessage('DOM:WebManifest:hasManifestLink', response);
+  }),
 
-function cloneError(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)
-  };
-  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);
+  /**
+   * Obtains a web manifest from content by using the ManifestObtainer
+   * and messages back the result.
+   * @param {Object} aMsg The IPC message.
+   */
+  obtainManifest: Task.async(function* ({data: {id}}) {
+    const obtainer = new ManifestObtainer();
+    const response = this.makeMsgResponse(id);
+    try {
+      response.result = yield obtainer.obtainManifest(content);
+      response.success = true;
+    } catch (err) {
+      response.result = this.serializeError(err);
     }
-    // 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'
+    sendAsyncMessage('DOM:ManifestObtainer:Obtain', response);
+  }),
+
+  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
+  /**
+   * 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.
+   */
+  serializeError(aError) {
+    const clone = {
+      'fileName': aError.fileName,
+      'lineNumber': aError.lineNumber,
+      'columnNumber': aError.columnNumber,
+      'stack': aError.stack,
+      'message': aError.message,
+      'name': aError.name
     };
-    const processor = new ManifestProcessor();
-    const manifest = processor.process(args);
-    return Cu.cloneInto(manifest, content);
-  });
-}
+    return clone;
+  },
+};
+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,58 @@
+/* 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');
+
+/**
+ * @constructor
+ */
+function ManifestFinder() {}
+
+/**
+ * checks if a browser window's document has a conforming
+ * manifest link relationship.
+ * @param aWindowOrBrowser the XUL browser or window to check.
+ * @return {Promise}
+ */
+ManifestFinder.prototype.hasManifestLink = Task.async(
+  function* (aWindowOrBrowser) {
+    const msgKey = 'DOM:WebManifest:hasManifestLink';
+    if (!(aWindowOrBrowser && (aWindowOrBrowser.namespaceURI || aWindowOrBrowser.location))) {
+      throw new TypeError('Invalid input.');
+    }
+    if (isXULBrowser(aWindowOrBrowser)) {
+      const mm = aWindowOrBrowser.messageManager;
+      const reply = yield PromiseMessage.send(mm, msgKey);
+      return reply.data.result;
+    }
+    return checkForManifest(aWindowOrBrowser);
+  }
+);
+
+function isXULBrowser(aBrowser) {
+  const XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+  return (aBrowser.namespaceURI && aBrowser.namespaceURI === XUL);
+}
+
+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,170 @@
+/* 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(Window)`, which returns
+ * a promise. If successful, you get back a manifest object.
+ *
+ * Import it with URL:
+ *   'chrome://global/content/manifestMessages.js'
+ *
+ * e10s IPC messaage 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');
+
+const processor = new ManifestProcessor();
+
+/**
+ * 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 = processor.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;
+}
+
+/**
+ * ManifestObtainer
+ * @constructor
+ */
+function ManifestObtainer() {
+}
+
+/**
+ * Public interface for obtaining a web manifest.
+ * @param  {XULWindow or DOMWindow} aWindow The Window from which to fetch
+ *                                          the manifest.
+ * @return {Promise<Object>} The processed manifest.
+ */
+ManifestObtainer.prototype.obtainManifest = Task.async(
+  function* (aWindowOrBrowser) {
+    const msgKey = 'DOM:ManifestObtainer:Obtain';
+    if (!(aWindowOrBrowser && (aWindowOrBrowser.namespaceURI || aWindowOrBrowser.location))) {
+      throw new TypeError('Invalid input.');
+    }
+    if (isXULBrowser(aWindowOrBrowser)) {
+      const mm = aWindowOrBrowser.messageManager;
+      const {data: {success, result}} = yield PromiseMessage.send(mm, msgKey);
+      if (!success) {
+        const error = toError(result);
+        throw error;
+      }
+      return result;
+    }
+    const manifest = yield fetchManifest(aWindowOrBrowser);
+    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) {
+  const XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+  return (aBrowser.namespaceURI && aBrowser.namespaceURI === XUL);
+}
+
+this.ManifestObtainer = ManifestObtainer; // jshint ignore:line
+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,43 +14,35 @@
  *
  * 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', {});
+Cu.import('resource://gre/modules/ImageObjectProcessor.jsm');
 
 function ManifestProcessor() {}
 
 // Static getters
 Object.defineProperties(ManifestProcessor, {
   'defaultDisplayMode': {
     get: function() {
       return 'browser';
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_hasManifestLink.js]
+[browser_ManifestObtainer_obtain.js]
--- a/dom/manifest/test/browser_ManifestObtainer_obtain.js
+++ b/dom/manifest/test/browser_ManifestObtainer_obtain.js
@@ -1,14 +1,14 @@
 //Used by JSHint:
 /*global 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.
new file mode 100644
--- /dev/null
+++ b/dom/manifest/test/browser_hasManifestLink.js
@@ -0,0 +1,109 @@
+//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 finder = new ManifestFinder();
+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 finder.hasManifestLink(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 finder.hasManifestLink(aBrowser);
+    aTest.run(result);
+  }
+});
--- a/dom/manifest/test/common.js
+++ b/dom/manifest/test/common.js
@@ -1,16 +1,16 @@
 /**
  * Common infrastructure for manifest tests.
  **/
-
+/*globals SpecialPowers, ManifestProcessor*/
 'use strict';
 const {
   ManifestProcessor
-} = SpecialPowers.Cu.import('resource://gre/modules/WebManifest.jsm');
+} = SpecialPowers.Cu.import('resource://gre/modules/ManifestProcessor.jsm');
 const processor = new 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
@@ -1,30 +1,24 @@
 /*
  * Description of the tests:
  *   These tests check for conformance to the CSP spec as they relate to Web Manifests.
  *
  *   In particular, the tests check that default-src and manifest-src directives are
  *   are respected by the ManifestObtainer.
  */
-/*globals Components*/
+/*globals SpecialPowers, requestLongerTimeout, ok, Cu, is, add_task, gBrowser, BrowserTestUtils, ManifestObtainer*/
 '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', this); // jshint ignore:line
 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`;
-const secureURL = `https://example.com${path}${server}`;
 const tests = [
   // CSP block everything, so trying to load a manifest
   // will result in a policy violation.
   {
     expected: `default-src 'none' blocks fetching manifest.`,
     get tabURL() {
       let queryParts = [
         `csp=default-src 'none'`,
@@ -243,17 +237,17 @@ add_task(function* () {
 
 // Helper object used to observe policy violations. It waits 10 seconds
 // for a response, and then times out causing its associated test to fail.
 function NetworkObserver(test) {
   let finishedTest;
   let success = false;
   this.finished = new Promise((resolver) => {
     finishedTest = resolver;
-  })
+  });
   this.observe = function observer(subject, topic) {
     SpecialPowers.removeObserver(this, 'csp-on-violate-policy');
     test.run(topic);
     finishedTest();
     success = true;
   };
   SpecialPowers.addObserver(this, 'csp-on-violate-policy', false);
   setTimeout(() => {
--- 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,19 @@
 /*
  * Description of the test:
  *   Check that mixed content blocker works prevents fetches of
  *   mixed content manifests.
  */
+/*globals Cu, add_task, ok, gBrowser, BrowserTestUtils, ManifestObtainer*/
 'use strict';
 const {
   ManifestObtainer
-} = Components.utils.import('resource://gre/modules/WebManifest.jsm', {});
+} = Cu.import('resource://gre/modules/ManifestObtainer.jsm', this); // jshint ignore:line
+const obtainer = new ManifestObtainer();
 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.
   {
@@ -26,30 +28,30 @@ const tests = [
       // Check reason for error.
       const check = /blocked the loading of a resource/.test(error.message);
       ok(check, this.expected);
     }
   }
 ];
 
 //jscs:disable
-add_task(function*() {
+add_task(function* () {
   //jscs:enable
   for (let test of tests) {
     let tabOptions = {
       gBrowser: gBrowser,
       url: test.tabURL,
     };
     yield BrowserTestUtils.withNewTab(
       tabOptions,
       browser => testObtainingManifest(browser, test)
     );
   }
 
   function* testObtainingManifest(aBrowser, aTest) {
-    const obtainer = new ManifestObtainer();
+    let manifest;
     try {
       yield obtainer.obtainManifest(aBrowser);
     } catch (e) {
-      aTest.run(e)
+      return 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',