Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 03 Jul 2014 16:35:21 -0400
changeset 192162 613bc15ccf05629e0d0e7c5f7e53a44aa99ded41
parent 192142 9c43461f16ba39052fe71eec767ef6dfab67f0eb (current diff)
parent 192161 610cfd0139c1a495613ac7dbe568f73ab33d09fe (diff)
child 192236 06e9a27a6fcc5e665588fa4f670d141fd9bac694
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmerge
milestone33.0a1
Merge fx-team to m-c. a=merge
browser/components/loop/content/shared/js/client.js
browser/components/loop/content/shared/libs/hawk-browser-2.2.1.js
browser/components/loop/content/shared/libs/sjcl-dev20140604.js
browser/components/loop/content/shared/libs/token.js
browser/components/loop/test/shared/client_test.js
--- a/browser/base/content/newtab/sites.js
+++ b/browser/base/content/newtab/sites.js
@@ -153,17 +153,17 @@ Site.prototype = {
    * Refreshes the thumbnail for the site.
    */
   refreshThumbnail: function Site_refreshThumbnail() {
     let thumbnail = this._querySelector(".newtab-thumbnail");
     if (this.link.bgColor) {
       thumbnail.style.backgroundColor = this.link.bgColor;
     }
     let uri = this.link.imageURI || PageThumbs.getThumbnailURL(this.url);
-    thumbnail.style.backgroundImage = "url(" + uri + ")";
+    thumbnail.style.backgroundImage = 'url("' + uri + '")';
   },
 
   /**
    * Adds event handlers for the site and its buttons.
    */
   _addEventHandlers: function Site_addEventHandlers() {
     // Register drag-and-drop event handlers.
     this._node.addEventListener("dragstart", this, false);
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -38,29 +38,16 @@ function injectLoopAPI(targetWindow) {
         return MozLoopService.doNotDisturb;
       },
       set: function(aFlag) {
         MozLoopService.doNotDisturb = aFlag;
       }
     },
 
     /**
-     * Returns the url for the Loop server from preferences.
-     *
-     * @return {String} The Loop server url
-     */
-    serverUrl: {
-      enumerable: true,
-      configurable: true,
-      get: function() {
-        return Services.prefs.getCharPref("loop.server");
-      }
-    },
-
-    /**
      * Returns the current locale of the browser.
      *
      * @returns {String} The locale string
      */
     locale: {
       enumerable: true,
       configurable: true,
       get: function() {
@@ -209,17 +196,50 @@ function injectLoopAPI(targetWindow) {
                                                     ringerStopper);
           ringerStopper = null;
         }
         if (ringer) {
           ringer.pause();
           ringer = null;
         }
       }
-    }
+    },
+
+    /**
+     * Performs a hawk based request to the loop server.
+     *
+     * Callback parameters:
+     *  - {Object|null} null if success. Otherwise an object:
+     *    {
+     *      code: 401,
+     *      errno: 401,
+     *      error: "Request failed",
+     *      message: "invalid token"
+     *    }
+     *  - {String} The body of the response.
+     *
+     * @param {String} path The path to make the request to.
+     * @param {String} method The request method, e.g. 'POST', 'GET'.
+     * @param {Object} payloadObj An object which is converted to JSON and
+     *                            transmitted with the request.
+     * @param {Function} callback Called when the request completes.
+     */
+    hawkRequest: {
+      enumerable: true,
+      configurable: true,
+      writable: true,
+      value: function(path, method, payloadObj, callback) {
+        // XXX Should really return a DOM promise here.
+        return MozLoopService.hawkRequest(path, method, payloadObj).then((response) => {
+          callback(null, response.body);
+        }, (error) => {
+          callback(Cu.cloneInto(error, targetWindow));
+        });
+      }
+    },
   };
 
   let contentObj = Cu.createObjectIn(targetWindow);
   Object.defineProperties(contentObj, api);
   Cu.makeObjectPropsNormal(contentObj);
 
   targetWindow.navigator.wrappedJSObject.__defineGetter__("mozLoop", function() {
     // We do this in a getter, so that we create these objects
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -142,32 +142,39 @@ let MozLoopServiceInternal = {
 
   /**
    * Performs a hawk based request to the loop server.
    *
    * @param {String} path The path to make the request to.
    * @param {String} method The request method, e.g. 'POST', 'GET'.
    * @param {Object} payloadObj An object which is converted to JSON and
    *                            transmitted with the request.
+   * @returns {Promise}
+   *        Returns a promise that resolves to the response of the API call,
+   *        or is rejected with an error.  If the server response can be parsed
+   *        as JSON and contains an 'error' property, the promise will be
+   *        rejected with this JSON-parsed response.
    */
   hawkRequest: function(path, method, payloadObj) {
     if (!this._hawkClient) {
       this._hawkClient = new HawkClient(this.loopServerUri);
     }
 
     let sessionToken;
     try {
       sessionToken = Services.prefs.getCharPref("loop.hawk-session-token");
     } catch (x) {
       // It is ok for this not to exist, we'll default to sending no-creds
     }
 
     let credentials;
     if (sessionToken) {
-      credentials = deriveHawkCredentials(sessionToken, "sessionToken", 2 * 32);
+      // true = use a hex key, as required by the server (see bug 1032738).
+      credentials = deriveHawkCredentials(sessionToken, "sessionToken",
+                                          2 * 32, true);
     }
 
     return this._hawkClient.request(path, method, credentials, payloadObj);
   },
 
   /**
    * Used to store a session token from a request if it exists in the headers.
    *
@@ -476,10 +483,27 @@ this.MozLoopService = {
   getLoopCharPref: function(prefName) {
     try {
       return Services.prefs.getCharPref("loop." + prefName);
     } catch (ex) {
       console.log("getLoopCharPref had trouble getting " + prefName +
         "; exception: " + ex);
       return null;
     }
-  }
+  },
+
+  /**
+   * Performs a hawk based request to the loop server.
+   *
+   * @param {String} path The path to make the request to.
+   * @param {String} method The request method, e.g. 'POST', 'GET'.
+   * @param {Object} payloadObj An object which is converted to JSON and
+   *                            transmitted with the request.
+   * @returns {Promise}
+   *        Returns a promise that resolves to the response of the API call,
+   *        or is rejected with an error.  If the server response can be parsed
+   *        as JSON and contains an 'error' property, the promise will be
+   *        rejected with this JSON-parsed response.
+   */
+  hawkRequest: function(path, method, payloadObj) {
+    return MozLoopServiceInternal.hawkRequest(path, method, payloadObj);
+  },
 };
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -16,20 +16,17 @@
 
     <div id="main"></div>
 
     <script type="text/javascript" src="loop/libs/l10n.js"></script>
     <script type="text/javascript" src="loop/libs/sdk.js"></script>
     <script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
-    <script type="text/javascript" src="loop/shared/libs/sjcl-dev20140604.js"></script>
-    <script type="text/javascript" src="loop/shared/libs/token.js"></script>
-    <script type="text/javascript" src="loop/shared/libs/hawk-browser-2.2.1.js"></script>
 
-    <script type="text/javascript" src="loop/shared/js/client.js"></script>
     <script type="text/javascript" src="loop/shared/js/models.js"></script>
     <script type="text/javascript" src="loop/shared/js/router.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
+    <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript" src="loop/js/desktopRouter.js"></script>
     <script type="text/javascript" src="loop/js/conversation.js"></script>
   </body>
 </html>
rename from browser/components/loop/content/shared/js/client.js
rename to browser/components/loop/content/js/client.js
--- a/browser/components/loop/content/shared/js/client.js
+++ b/browser/components/loop/content/js/client.js
@@ -1,30 +1,30 @@
 /* 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/. */
 
 /* global loop:true, hawk, deriveHawkCredentials */
 
 var loop = loop || {};
-loop.shared = loop.shared || {};
-loop.shared.Client = (function($) {
+loop.Client = (function($) {
   "use strict";
 
+  // The expected properties to be returned from the POST /call-url/ request.
+  const expectedCallUrlProperties = ["callUrl", "expiresAt"];
+
+  // The expected properties to be returned from the GET /calls request.
+  const expectedCallProperties = ["calls"];
+
   /**
    * Loop server client.
    *
    * @param {Object} settings Settings object.
    */
-  function Client(settings) {
-    settings = settings || {};
-    if (!settings.hasOwnProperty("baseServerUrl") ||
-        !settings.baseServerUrl) {
-      throw new Error("missing required baseServerUrl");
-    }
+  function Client(settings = {}) {
 
     // allowing an |in| test rather than a more type || allows us to dependency
     // inject a non-existent mozLoop
     if ("mozLoop" in settings) {
       this.mozLoop = settings.mozLoop;
     } else {
       this.mozLoop = navigator.mozLoop;
     }
@@ -55,158 +55,86 @@ loop.shared.Client = (function($) {
 
       properties.forEach(function (property) {
         if (!data.hasOwnProperty(property)) {
           throw new Error("Invalid data received from server - missing " +
             property);
         }
       });
 
-      if (properties.length <= 1) {
+      if (properties.length == 1) {
         return data[properties[0]];
       }
 
       return data;
     },
 
     /**
      * Generic handler for XHR failures.
      *
      * @param {Function} cb Callback(err)
-     * @param jqXHR See jQuery docs
-     * @param textStatus See jQuery docs
-     * @param errorThrown See jQuery docs
+     * @param {Object} error See MozLoopAPI.hawkRequest
      */
-    _failureHandler: function(cb, jqXHR, textStatus, errorThrown) {
-      var error = "Unknown error.",
-          jsonRes = jqXHR && jqXHR.responseJSON || {};
-      // Received error response format:
-      // { "status": "errors",
-      //   "errors": [{
-      //      "location": "url",
-      //      "name": "token",
-      //      "description": "invalid token"
-      // }]}
-      if (jsonRes.status === "errors" && Array.isArray(jsonRes.errors)) {
-        error = "Details: " + jsonRes.errors.map(function(err) {
-          return Object.keys(err).map(function(field) {
-            return field + ": " + err[field];
-          }).join(", ");
-        }).join("; ");
-      }
-      var message = "HTTP " + jqXHR.status + " " + errorThrown +
-          "; " + error;
+    _failureHandler: function(cb, error) {
+      var message = "HTTP " + error.code + " " + error.error + "; " + error.message;
       console.error(message);
       cb(new Error(message));
     },
 
     /**
      * Ensures the client is registered with the push server.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      *
      * @param {Function} cb Callback(err)
      */
     _ensureRegistered: function(cb) {
-      navigator.mozLoop.ensureRegistered(function(err) {
-        cb(err);
-      }.bind(this));
-    },
-
-    /**
-     * Ensures that the client picks up the hawk-session-token
-     * put in preferences by the LoopService registration code,
-     * derives hawk credentials from them, and saves them in
-     * this._credentials.
-     *
-     * @param {Function} cb Callback(err)
-     *  if err is set to null in the callback, that indicates that the
-     *  credentials have been successfully attached to this object.
-     *
-     * @private
-     *
-     * @note That as currently written, this is only ever expected to be called
-     * from browser UI code (ie it relies on mozLoop).
-     */
-    _ensureCredentials: function(cb) {
-      if (this._credentials) {
-        cb(null);
-        return;
-      }
-
-      var hawkSessionToken =
-        this.mozLoop.getLoopCharPref("hawk-session-token");
-      if (!hawkSessionToken) {
-        var msg = "loop.hawk-session-token pref not found";
-        console.warn(msg);
-        cb(new Error(msg));
-        return;
-      }
-
-      // XXX do we want to use any of the other hawk params (eg to track clock
-      // skew, etc)?
-      var serverDerivedKeyLengthInBytes = 2 * 32;
-      deriveHawkCredentials(hawkSessionToken, "sessionToken",
-        serverDerivedKeyLengthInBytes, function (hawkCredentials) {
-          this._credentials = hawkCredentials;
-          cb(null);
-        }.bind(this));
+      this.mozLoop.ensureRegistered(cb);
     },
 
     /**
      * Internal handler for requesting a call url from the server.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      * - callUrlData an object of the obtained call url data if successful:
      * -- call_url: The url of the call
      * -- expiresAt: The amount of hours until expiry of the url
      *
      * @param  {String} simplepushUrl a registered Simple Push URL
      * @param  {string} nickname the nickname of the future caller
      * @param  {Function} cb Callback(err, callUrlData)
      */
     _requestCallUrlInternal: function(nickname, cb) {
-      var endpoint = this.settings.baseServerUrl + "/call-url/",
-          reqData  = {callerId: nickname};
+      this.mozLoop.hawkRequest("/call-url/", "POST", {callerId: nickname},
+                               (error, responseText) => {
+        if (error) {
+          this._failureHandler(cb, error);
+          return;
+        }
+
+        try {
+          var urlData = JSON.parse(responseText);
 
-      var req = $.ajax({
-        type: "POST",
-        url: endpoint,
-        data: reqData,
-        xhrFields: {
-          withCredentials: false
-        },
-        crossDomain: true,
-        beforeSend: function (xhr, settings) {
-          try {
-            this._attachAnyServerCreds(xhr, settings);
-          } catch (ex) {
-            cb(ex);
-            return false;
-          }
-          return true;
-        }.bind(this),
-        success: function(callUrlData) {
-          // XXX split this out into two functions for better readability
-          try {
-            cb(null, this._validate(callUrlData, ["call_url", "expiresAt"]));
+          // XXX Support an alternate call_url property for
+          // backwards compatibility whilst we switch over servers.
+          // Bug 1033988 will want to remove these two lines.
+          if (urlData.call_url)
+            urlData.callUrl = urlData.call_url;
+
+          cb(null, this._validate(urlData, expectedCallUrlProperties));
 
-            var expiresHours = this._hoursToSeconds(callUrlData.expiresAt);
-            navigator.mozLoop.noteCallUrlExpiry(expiresHours);
-          } catch (err) {
-            console.log("Error requesting call info", err);
-            cb(err);
-          }
-        }.bind(this),
-        dataType: "json"
+          var expiresHours = this._hoursToSeconds(urlData.expiresAt);
+          this.mozLoop.noteCallUrlExpiry(expiresHours);
+        } catch (err) {
+          console.log("Error requesting call info", err);
+          cb(err);
+        }
       });
-
-      req.fail(this._failureHandler.bind(this, cb));
     },
 
     /**
      * Requests a call URL from the Loop server. It will note the
      * expiry time for the url with the mozLoop api.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
@@ -220,143 +148,49 @@ loop.shared.Client = (function($) {
      */
     requestCallUrl: function(nickname, cb) {
       this._ensureRegistered(function(err) {
         if (err) {
           console.log("Error registering with Loop server, code: " + err);
           cb(err);
           return;
         }
-        this._ensureCredentials(function (err) {
-          if (err) {
-            console.log("Error setting up credentials: " + err);
-            cb(err);
-            return;
-          }
-          this._requestCallUrlInternal(nickname, cb);
-        }.bind(this));
+
+        this._requestCallUrlInternal(nickname, cb);
       }.bind(this));
     },
 
     /**
      * Requests call information from the server for all calls since the
      * given version.
      *
      * @param  {String} version the version identifier from the push
      *                          notification
      * @param  {Function} cb Callback(err, calls)
      */
     requestCallsInfo: function(version, cb) {
-      this._ensureCredentials(function (err) {
-        if (err) {
-          console.log("Error setting up credentials: " + err);
-          cb(err);
-          return;
-        }
-        this._requestCallsInfoInternal(version, cb);
-      }.bind(this));
-    },
-
-    _requestCallsInfoInternal: function(version, cb) {
+      // XXX It is likely that we'll want to move some of this to whatever
+      // opens the chat window, but we'll need to decide on this in bug 1002418
       if (!version) {
         throw new Error("missing required parameter version");
       }
 
-      var endpoint = this.settings.baseServerUrl + "/calls";
+      this.mozLoop.hawkRequest("/calls?version=" + version, "GET", null,
+                               (error, responseText) => {
+        if (error) {
+          this._failureHandler(cb, error);
+          return;
+        }
 
-      // XXX It is likely that we'll want to move some of this to whatever
-      // opens the chat window, but we'll need to decide that once we make a
-      // decision on chrome versus content, and know if we're going with
-      // LoopService or a frameworker.
-      var req = $.ajax({
-        type: "GET",
-        url: endpoint + "?version=" + version,
-        xhrFields: {
-          withCredentials: false
-        },
-        crossDomain: true,
-        beforeSend: function (xhr, settings) {
-          try {
-            this._attachAnyServerCreds(xhr, settings);
-          } catch (ex) {
-            cb(ex);
-            return false;
-          }
-          return true;
-        }.bind(this),
-        success: function(callsData) {
-          try {
-            cb(null, this._validate(callsData, ["calls"]));
-          } catch (err) {
-            console.log("Error requesting calls info", err);
-            cb(err);
-          }
-        }.bind(this),
-        dataType: "json"
-      });
+        try {
+          var callsData = JSON.parse(responseText);
 
-      req.fail(this._failureHandler.bind(this, cb));
-    },
-
-    /**
-     * Posts a call request to the server for a call represented by the
-     * loopToken. Will return the session data for the call.
-     *
-     * @param  {String} loopToken The loopToken representing the call
-     * @param  {Function} cb Callback(err, sessionData)
-     */
-    requestCallInfo: function(loopToken, cb) {
-      if (!loopToken) {
-        throw new Error("missing required parameter loopToken");
-      }
-
-      var req = $.ajax({
-        url:         this.settings.baseServerUrl + "/calls/" + loopToken,
-        method:      "POST",
-        contentType: "application/json",
-        data:        JSON.stringify({}),
-        dataType:    "json"
-      });
-
-      req.done(function(sessionData) {
-        try {
-          cb(null, this._validate(sessionData, [
-            "sessionId", "sessionToken", "apiKey"
-          ]));
+          cb(null, this._validate(callsData, expectedCallProperties));
         } catch (err) {
-          console.log("Error requesting call info", err);
+          console.log("Error requesting calls info", err);
           cb(err);
         }
-      }.bind(this));
-
-      req.fail(this._failureHandler.bind(this, cb));
+      });
     },
-
-    /**
-     * If this._credentials is set, adds a hawk Authorization header based
-     * based on those credentials to the passed-in XHR.
-     *
-     * @param xhr        request to add any header to
-     * @param settings   settings object passed to jQuery.ajax()
-     * @private
-     */
-    _attachAnyServerCreds: function(xhr, settings) {
-      // if the server needs credentials and didn't get them, it will
-      // return failure for us, so if we don't have any creds, don't try to
-      // attach them.
-      if (!this._credentials) {
-        return;
-      }
-
-      var header = hawk.client.header(settings.url, settings.type,
-        { credentials: this._credentials });
-      if (header.err) {
-        throw new Error(header.err);
-      }
-
-      xhr.setRequestHeader("Authorization", header.field);
-
-      return;
-    }
   };
 
   return Client;
 })(jQuery);
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -132,17 +132,17 @@ loop.conversation = (function(OT, mozL10
     },
 
     /**
      * Accepts an incoming call.
      */
     accept: function() {
       window.navigator.mozLoop.stopAlerting();
       this._conversation.initiate({
-        baseServerUrl: window.navigator.mozLoop.serverUrl,
+        client: new loop.Client(),
         outgoing: false
       });
     },
 
     /**
      * Declines an incoming call.
      */
     decline: function() {
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -44,30 +44,49 @@ loop.panel = (function(_, mozL10n) {
     render: function() {
       this.$el.html(this.template({
         checked: navigator.mozLoop.doNotDisturb ? "checked" : ""
       }));
       return this;
     }
   });
 
+  var ToSView = sharedViews.BaseView.extend({
+    template: _.template([
+      '<p data-l10n-id="legal_text_and_links"',
+      '  data-l10n-args=\'',
+      '    {"terms_of_use_url": "https://accounts.firefox.com/legal/terms",',
+      '     "privacy_notice_url": "www.mozilla.org/privacy/"',
+      '    }\'></p>'
+    ].join('')),
+
+    render: function() {
+      if (navigator.mozLoop.getLoopCharPref('seenToS') === null) {
+        this.$el.html(this.template());
+        navigator.mozLoop.setLoopCharPref('seenToS', 'seen');
+      }
+      return this;
+    }
+  });
+
   /**
    * Panel view.
    */
   var PanelView = sharedViews.BaseView.extend({
     template: _.template([
       '<div class="description">',
       '  <p data-l10n-id="get_link_to_share"></p>',
       '</div>',
       '<div class="action">',
       '  <form class="invite">',
       '    <input type="text" name="caller" data-l10n-id="caller" required>',
       '    <button type="submit" class="get-url btn btn-success"',
       '       data-l10n-id="get_a_call_url"></button>',
       '  </form>',
+      '  <p class="tos"></p>',
       '  <p class="result hide">',
       '    <input id="call-url" type="url" readonly>',
       '    <a class="go-back btn btn-info" href="" data-l10n-id="new_url"></a>',
       '  </p>',
       '  <p class="dnd"></p>',
       '</div>',
     ].join("")),
 
@@ -86,19 +105,17 @@ loop.panel = (function(_, mozL10n) {
     },
 
     initialize: function(options) {
       options = options || {};
       if (!options.notifier) {
         throw new Error("missing required notifier");
       }
       this.notifier = options.notifier;
-      this.client = new loop.shared.Client({
-        baseServerUrl: navigator.mozLoop.serverUrl
-      });
+      this.client = new loop.Client();
     },
 
     getNickname: function() {
       return this.$("input[name=caller]").val();
     },
 
     getCallUrl: function(event) {
       this.notifier.clear();
@@ -124,17 +141,17 @@ loop.panel = (function(_, mozL10n) {
       this.$(".description p").text(__("get_link_to_share"));
       this.changeButtonState();
     },
 
     onCallUrlReceived: function(callUrlData) {
       this.notifier.clear();
       this.$(".action .invite").hide();
       this.$(".action .invite input").val("");
-      this.$(".action .result input").val(callUrlData.call_url);
+      this.$(".action .result input").val(callUrlData.callUrl);
       this.$(".action .result").show();
       this.$(".description p").text(__("share_link_url"));
     },
 
     setPending: function() {
       this.$("[name=caller]").addClass("pending");
       this.$(".get-url").addClass("disabled").attr("disabled", "disabled");
     },
@@ -153,16 +170,17 @@ loop.panel = (function(_, mozL10n) {
         this.$(".get-url").addClass("disabled").attr("disabled", "disabled");
       }
     },
 
     render: function() {
       this.$el.html(this.template());
       // Do not Disturb sub view
       this.dndView = new DoNotDisturbView({el: this.$(".dnd")}).render();
+      this.tosView = new ToSView({el: this.$(".tos")}).render();
       return this;
     }
   });
 
   var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
     /**
      * DOM document object.
      * @type {HTMLDocument}
@@ -238,11 +256,12 @@ loop.panel = (function(_, mozL10n) {
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
   }
 
   return {
     init: init,
     PanelView: PanelView,
     DoNotDisturbView: DoNotDisturbView,
-    PanelRouter: PanelRouter
+    PanelRouter: PanelRouter,
+    ToSView: ToSView
   };
 })(_, document.mozL10n);
--- a/browser/components/loop/content/panel.html
+++ b/browser/components/loop/content/panel.html
@@ -14,20 +14,17 @@
     <div id="messages"></div>
 
     <div id="main"></div>
 
     <script type="text/javascript" src="loop/libs/l10n.js"></script>
     <script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
-    <script type="text/javascript" src="loop/shared/libs/sjcl-dev20140604.js"></script>
-    <script type="text/javascript" src="loop/shared/libs/token.js"></script>
-    <script type="text/javascript" src="loop/shared/libs/hawk-browser-2.2.1.js"></script>
 
-    <script type="text/javascript" src="loop/shared/js/client.js"></script>
     <script type="text/javascript" src="loop/shared/js/models.js"></script>
     <script type="text/javascript" src="loop/shared/js/router.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
+    <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript" src="loop/js/desktopRouter.js"></script>
     <script type="text/javascript" src="loop/js/panel.js"></script>
  </body>
 </html>
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -1,14 +1,18 @@
 /* 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/. */
 
 /* Panel styles */
 
+a {
+  color: #0095DD;
+}
+
 .panel {
   /* XXX the Social API panel behaves weirdly on inner element size changes,
      adding unwanted scrollbars; quickfix is to hide these for now. */
   overflow: hidden;
 }
 
 .spacer {
   margin-bottom: 1em;
@@ -71,13 +75,20 @@
 
 /* For some reason, buttons have a bigger default font size in FF; we're
    reducing a bit for graphical consistency here. */
 .share .action button {
   font-size: .9em;
   padding-top: 6px;
 }
 
+.tos {
+  font-size: .6rem;
+  color: #a8a8a8;
+  text-align: center;
+  padding: 1rem;
+}
+
 /* Specific cases */
 
 .panel #messages .alert {
   margin-bottom: 0;
 }
--- a/browser/components/loop/content/shared/js/models.js
+++ b/browser/components/loop/content/shared/js/models.js
@@ -57,33 +57,33 @@ loop.shared.models = (function() {
 
     /**
      * Initiates a conversation, requesting call session information to the Loop
      * server and updates appropriately the current model attributes with the
      * data.
      *
      * Available options:
      *
-     * - {String} baseServerUrl The server URL
      * - {Boolean} outgoing Set to true if this model represents the
      *                            outgoing call.
+     * - {Boolean} callType Only valid for outgoing calls. The type of media in
+     *                      the call, e.g. "audio" or "audio-video"
+     * - {loop.shared.Client} client  A client object to request call information
+     *                                from. Expects requestCallInfo for outgoing
+     *                                calls, requestCallsInfo for incoming calls.
      *
      * Triggered events:
      *
      * - `session:ready` when the session information have been successfully
      *   retrieved from the server;
      * - `session:error` when the request failed.
      *
      * @param {Object} options Options object
      */
     initiate: function(options) {
-      var client = new loop.shared.Client({
-        baseServerUrl: options.baseServerUrl
-      });
-
       function handleResult(err, sessionData) {
         /*jshint validthis:true */
         if (err) {
           this.trigger("session:error", new Error(
             "Retrieval of session information failed: HTTP " + err));
           return;
         }
 
@@ -94,20 +94,21 @@ loop.shared.models = (function() {
         // Bug 990714 should fix this.
         if (!options.outgoing)
           sessionData = sessionData[0];
 
         this.setReady(sessionData);
       }
 
       if (options.outgoing) {
-        client.requestCallInfo(this.get("loopToken"), handleResult.bind(this));
+        options.client.requestCallInfo(this.get("loopToken"), options.callType,
+          handleResult.bind(this));
       }
       else {
-        client.requestCallsInfo(this.get("loopVersion"),
+        options.client.requestCallsInfo(this.get("loopVersion"),
           handleResult.bind(this));
       }
     },
 
     /**
      * Checks that the session is ready.
      *
      * @return {Boolean}
deleted file mode 100644
--- a/browser/components/loop/content/shared/libs/hawk-browser-2.2.1.js
+++ /dev/null
@@ -1,556 +0,0 @@
-/*
-    HTTP Hawk Authentication Scheme
-    Copyright (c) 2012-2014, Eran Hammer <eran@hammer.io>
-    BSD Licensed
-*/
-
-
-// Declare namespace
-
-var hawk = {
-    internals: {}
-};
-
-
-hawk.client = {
-
-    // Generate an Authorization header for a given request
-
-    /*
-        uri: 'http://example.com/resource?a=b' or object generated by hawk.utils.parseUri()
-        method: HTTP verb (e.g. 'GET', 'POST')
-        options: {
-
-            // Required
-
-            credentials: {
-                id: 'dh37fgj492je',
-                key: 'aoijedoaijsdlaksjdl',
-                algorithm: 'sha256'                                 // 'sha1', 'sha256'
-            },
-
-            // Optional
-
-            ext: 'application-specific',                        // Application specific data sent via the ext attribute
-            timestamp: Date.now() / 1000,                       // A pre-calculated timestamp in seconds
-            nonce: '2334f34f',                                  // A pre-generated nonce
-            localtimeOffsetMsec: 400,                           // Time offset to sync with server time (ignored if timestamp provided)
-            payload: '{"some":"payload"}',                      // UTF-8 encoded string for body hash generation (ignored if hash provided)
-            contentType: 'application/json',                    // Payload content-type (ignored if hash provided)
-            hash: 'U4MKKSmiVxk37JCCrAVIjV=',                    // Pre-calculated payload hash
-            app: '24s23423f34dx',                               // Oz application id
-            dlg: '234sz34tww3sd'                                // Oz delegated-by application id
-        }
-    */
-
-    header: function (uri, method, options) {
-
-        var result = {
-            field: '',
-            artifacts: {}
-        };
-
-        // Validate inputs
-
-        if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') ||
-            !method || typeof method !== 'string' ||
-            !options || typeof options !== 'object') {
-
-            result.err = 'Invalid argument type';
-            return result;
-        }
-
-        // Application time
-
-        var timestamp = options.timestamp || hawk.utils.now(options.localtimeOffsetMsec);
-
-        // Validate credentials
-
-        var credentials = options.credentials;
-        if (!credentials ||
-            !credentials.id ||
-            !credentials.key ||
-            !credentials.algorithm) {
-
-            result.err = 'Invalid credentials object';
-            return result;
-        }
-
-        if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
-            result.err = 'Unknown algorithm';
-            return result;
-        }
-
-        // Parse URI
-
-        if (typeof uri === 'string') {
-            uri = hawk.utils.parseUri(uri);
-        }
-
-        // Calculate signature
-
-        var artifacts = {
-            ts: timestamp,
-            nonce: options.nonce || hawk.utils.randomString(6),
-            method: method,
-            resource: uri.relative,
-            host: uri.hostname,
-            port: uri.port,
-            hash: options.hash,
-            ext: options.ext,
-            app: options.app,
-            dlg: options.dlg
-        };
-
-        result.artifacts = artifacts;
-
-        // Calculate payload hash
-
-        if (!artifacts.hash &&
-            (options.payload || options.payload === '')) {
-
-            artifacts.hash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
-        }
-
-        var mac = hawk.crypto.calculateMac('header', credentials, artifacts);
-
-        // Construct header
-
-        var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== '';       // Other falsey values allowed
-        var header = 'Hawk id="' + credentials.id +
-                     '", ts="' + artifacts.ts +
-                     '", nonce="' + artifacts.nonce +
-                     (artifacts.hash ? '", hash="' + artifacts.hash : '') +
-                     (hasExt ? '", ext="' + hawk.utils.escapeHeaderAttribute(artifacts.ext) : '') +
-                     '", mac="' + mac + '"';
-
-        if (artifacts.app) {
-            header += ', app="' + artifacts.app +
-                      (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"';
-        }
-
-        result.field = header;
-
-        return result;
-    },
-
-
-    // Validate server response
-
-    /*
-        request:    object created via 'new XMLHttpRequest()' after response received
-        artifacts:  object received from header().artifacts
-        options: {
-            payload:    optional payload received
-            required:   specifies if a Server-Authorization header is required. Defaults to 'false'
-        }
-    */
-
-    authenticate: function (request, credentials, artifacts, options) {
-
-        options = options || {};
-
-        var getHeader = function (name) {
-
-            return request.getResponseHeader ? request.getResponseHeader(name) : request.getHeader(name);
-        };
-
-        var wwwAuthenticate = getHeader('www-authenticate');
-        if (wwwAuthenticate) {
-
-            // Parse HTTP WWW-Authenticate header
-
-            var attributes = hawk.utils.parseAuthorizationHeader(wwwAuthenticate, ['ts', 'tsm', 'error']);
-            if (!attributes) {
-                return false;
-            }
-
-            if (attributes.ts) {
-                var tsm = hawk.crypto.calculateTsMac(attributes.ts, credentials);
-                if (tsm !== attributes.tsm) {
-                    return false;
-                }
-
-                hawk.utils.setNtpOffset(attributes.ts - Math.floor((new Date()).getTime() / 1000));     // Keep offset at 1 second precision
-            }
-        }
-
-        // Parse HTTP Server-Authorization header
-
-        var serverAuthorization = getHeader('server-authorization');
-        if (!serverAuthorization &&
-            !options.required) {
-
-            return true;
-        }
-
-        var attributes = hawk.utils.parseAuthorizationHeader(serverAuthorization, ['mac', 'ext', 'hash']);
-        if (!attributes) {
-            return false;
-        }
-
-        var modArtifacts = {
-            ts: artifacts.ts,
-            nonce: artifacts.nonce,
-            method: artifacts.method,
-            resource: artifacts.resource,
-            host: artifacts.host,
-            port: artifacts.port,
-            hash: attributes.hash,
-            ext: attributes.ext,
-            app: artifacts.app,
-            dlg: artifacts.dlg
-        };
-
-        var mac = hawk.crypto.calculateMac('response', credentials, modArtifacts);
-        if (mac !== attributes.mac) {
-            return false;
-        }
-
-        if (!options.payload &&
-            options.payload !== '') {
-
-            return true;
-        }
-
-        if (!attributes.hash) {
-            return false;
-        }
-
-        var calculatedHash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, getHeader('content-type'));
-        return (calculatedHash === attributes.hash);
-    },
-
-    message: function (host, port, message, options) {
-
-        // Validate inputs
-
-        if (!host || typeof host !== 'string' ||
-            !port || typeof port !== 'number' ||
-            message === null || message === undefined || typeof message !== 'string' ||
-            !options || typeof options !== 'object') {
-
-            return null;
-        }
-
-        // Application time
-
-        var timestamp = options.timestamp || hawk.utils.now(options.localtimeOffsetMsec);
-
-        // Validate credentials
-
-        var credentials = options.credentials;
-        if (!credentials ||
-            !credentials.id ||
-            !credentials.key ||
-            !credentials.algorithm) {
-
-            // Invalid credential object
-            return null;
-        }
-
-        if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
-            return null;
-        }
-
-        // Calculate signature
-
-        var artifacts = {
-            ts: timestamp,
-            nonce: options.nonce || hawk.utils.randomString(6),
-            host: host,
-            port: port,
-            hash: hawk.crypto.calculatePayloadHash(message, credentials.algorithm)
-        };
-
-        // Construct authorization
-
-        var result = {
-            id: credentials.id,
-            ts: artifacts.ts,
-            nonce: artifacts.nonce,
-            hash: artifacts.hash,
-            mac: hawk.crypto.calculateMac('message', credentials, artifacts)
-        };
-
-        return result;
-    },
-
-    authenticateTimestamp: function (message, credentials, updateClock) {           // updateClock defaults to true
-
-        var tsm = hawk.crypto.calculateTsMac(message.ts, credentials);
-        if (tsm !== message.tsm) {
-            return false;
-        }
-
-        if (updateClock !== false) {
-            hawk.utils.setNtpOffset(message.ts - Math.floor((new Date()).getTime() / 1000));    // Keep offset at 1 second precision
-        }
-
-        return true;
-    }
-};
-
-
-hawk.crypto = {
-
-    headerVersion: '1',
-
-    algorithms: ['sha1', 'sha256'],
-
-    calculateMac: function (type, credentials, options) {
-
-        var normalized = hawk.crypto.generateNormalizedString(type, options);
-
-        var hmac = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()](normalized, credentials.key);
-        return hmac.toString(CryptoJS.enc.Base64);
-    },
-
-    generateNormalizedString: function (type, options) {
-
-        var normalized = 'hawk.' + hawk.crypto.headerVersion + '.' + type + '\n' +
-                         options.ts + '\n' +
-                         options.nonce + '\n' +
-                         (options.method || '').toUpperCase() + '\n' +
-                         (options.resource || '') + '\n' +
-                         options.host.toLowerCase() + '\n' +
-                         options.port + '\n' +
-                         (options.hash || '') + '\n';
-
-        if (options.ext) {
-            normalized += options.ext.replace('\\', '\\\\').replace('\n', '\\n');
-        }
-
-        normalized += '\n';
-
-        if (options.app) {
-            normalized += options.app + '\n' +
-                          (options.dlg || '') + '\n';
-        }
-
-        return normalized;
-    },
-
-    calculatePayloadHash: function (payload, algorithm, contentType) {
-
-        var hash = CryptoJS.algo[algorithm.toUpperCase()].create();
-        hash.update('hawk.' + hawk.crypto.headerVersion + '.payload\n');
-        hash.update(hawk.utils.parseContentType(contentType) + '\n');
-        hash.update(payload);
-        hash.update('\n');
-        return hash.finalize().toString(CryptoJS.enc.Base64);
-    },
-
-    calculateTsMac: function (ts, credentials) {
-
-        var hash = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()]('hawk.' + hawk.crypto.headerVersion + '.ts\n' + ts + '\n', credentials.key);
-        return hash.toString(CryptoJS.enc.Base64);
-    }
-};
-
-
-// localStorage compatible interface
-
-hawk.internals.LocalStorage = function () {
-
-    this._cache = {};
-    this.length = 0;
-
-    this.getItem = function (key) {
-
-        return this._cache.hasOwnProperty(key) ? String(this._cache[key]) : null;
-    };
-
-    this.setItem = function (key, value) {
-
-        this._cache[key] = String(value);
-        this.length = Object.keys(this._cache).length;
-    };
-
-    this.removeItem = function (key) {
-
-        delete this._cache[key];
-        this.length = Object.keys(this._cache).length;
-    };
-
-    this.clear = function () {
-
-        this._cache = {};
-        this.length = 0;
-    };
-
-    this.key = function (i) {
-
-        return Object.keys(this._cache)[i || 0];
-    };
-};
-
-
-hawk.utils = {
-
-    storage: new hawk.internals.LocalStorage(),
-
-    setStorage: function (storage) {
-
-        var ntpOffset = hawk.utils.storage.getItem('hawk_ntp_offset');
-        hawk.utils.storage = storage;
-        if (ntpOffset) {
-            hawk.utils.setNtpOffset(ntpOffset);
-        }
-    },
-
-    setNtpOffset: function (offset) {
-
-        try {
-            hawk.utils.storage.setItem('hawk_ntp_offset', offset);
-        }
-        catch (err) {
-            console.error('[hawk] could not write to storage.');
-            console.error(err);
-        }
-    },
-
-    getNtpOffset: function () {
-
-        var offset = hawk.utils.storage.getItem('hawk_ntp_offset');
-        if (!offset) {
-            return 0;
-        }
-
-        return parseInt(offset, 10);
-    },
-
-    now: function (localtimeOffsetMsec) {
-
-        return Math.floor(((new Date()).getTime() + (localtimeOffsetMsec || 0)) / 1000) + hawk.utils.getNtpOffset();
-    },
-
-    escapeHeaderAttribute: function (attribute) {
-
-        return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"');
-    },
-
-    parseContentType: function (header) {
-
-        if (!header) {
-            return '';
-        }
-
-        return header.split(';')[0].replace(/^\s+|\s+$/g, '').toLowerCase();
-    },
-
-    parseAuthorizationHeader: function (header, keys) {
-
-        if (!header) {
-            return null;
-        }
-
-        var headerParts = header.match(/^(\w+)(?:\s+(.*))?$/);       // Header: scheme[ something]
-        if (!headerParts) {
-            return null;
-        }
-
-        var scheme = headerParts[1];
-        if (scheme.toLowerCase() !== 'hawk') {
-            return null;
-        }
-
-        var attributesString = headerParts[2];
-        if (!attributesString) {
-            return null;
-        }
-
-        var attributes = {};
-        var verify = attributesString.replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, function ($0, $1, $2) {
-
-            // Check valid attribute names
-
-            if (keys.indexOf($1) === -1) {
-                return;
-            }
-
-            // Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9
-
-            if ($2.match(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/) === null) {
-                return;
-            }
-
-            // Check for duplicates
-
-            if (attributes.hasOwnProperty($1)) {
-                return;
-            }
-
-            attributes[$1] = $2;
-            return '';
-        });
-
-        if (verify !== '') {
-            return null;
-        }
-
-        return attributes;
-    },
-
-    randomString: function (size) {
-
-        var randomSource = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
-        var len = randomSource.length;
-
-        var result = [];
-        for (var i = 0; i < size; ++i) {
-            result[i] = randomSource[Math.floor(Math.random() * len)];
-        }
-
-        return result.join('');
-    },
-
-    parseUri: function (input) {
-
-        // Based on: parseURI 1.2.2
-        // http://blog.stevenlevithan.com/archives/parseuri
-        // (c) Steven Levithan <stevenlevithan.com>
-        // MIT License
-
-        var keys = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'hostname', 'port', 'resource', 'relative', 'pathname', 'directory', 'file', 'query', 'fragment'];
-
-        var uriRegex = /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?)(?:#(.*))?)/;
-        var uriByNumber = input.match(uriRegex);
-        var uri = {};
-
-        for (var i = 0, il = keys.length; i < il; ++i) {
-            uri[keys[i]] = uriByNumber[i] || '';
-        }
-
-        if (uri.port === '') {
-            uri.port = (uri.protocol.toLowerCase() === 'http' ? '80' : (uri.protocol.toLowerCase() === 'https' ? '443' : ''));
-        }
-
-        return uri;
-    }
-};
-
-
-// $lab:coverage:off$
-
-// Based on: Crypto-JS v3.1.2
-// Copyright (c) 2009-2013, Jeff Mott. All rights reserved.
-// http://code.google.com/p/crypto-js/
-// http://code.google.com/p/crypto-js/wiki/License
-
-var CryptoJS = CryptoJS || function (h, r) { var k = {}, l = k.lib = {}, n = function () { }, f = l.Base = { extend: function (a) { n.prototype = this; var b = new n; a && b.mixIn(a); b.hasOwnProperty("init") || (b.init = function () { b.$super.init.apply(this, arguments) }); b.init.prototype = b; b.$super = this; return b }, create: function () { var a = this.extend(); a.init.apply(a, arguments); return a }, init: function () { }, mixIn: function (a) { for (var b in a) a.hasOwnProperty(b) && (this[b] = a[b]); a.hasOwnProperty("toString") && (this.toString = a.toString) }, clone: function () { return this.init.prototype.extend(this) } }, j = l.WordArray = f.extend({ init: function (a, b) { a = this.words = a || []; this.sigBytes = b != r ? b : 4 * a.length }, toString: function (a) { return (a || s).stringify(this) }, concat: function (a) { var b = this.words, d = a.words, c = this.sigBytes; a = a.sigBytes; this.clamp(); if (c % 4) for (var e = 0; e < a; e++) b[c + e >>> 2] |= (d[e >>> 2] >>> 24 - 8 * (e % 4) & 255) << 24 - 8 * ((c + e) % 4); else if (65535 < d.length) for (e = 0; e < a; e += 4) b[c + e >>> 2] = d[e >>> 2]; else b.push.apply(b, d); this.sigBytes += a; return this }, clamp: function () { var a = this.words, b = this.sigBytes; a[b >>> 2] &= 4294967295 << 32 - 8 * (b % 4); a.length = h.ceil(b / 4) }, clone: function () { var a = f.clone.call(this); a.words = this.words.slice(0); return a }, random: function (a) { for (var b = [], d = 0; d < a; d += 4) b.push(4294967296 * h.random() | 0); return new j.init(b, a) } }), m = k.enc = {}, s = m.Hex = { stringify: function (a) { var b = a.words; a = a.sigBytes; for (var d = [], c = 0; c < a; c++) { var e = b[c >>> 2] >>> 24 - 8 * (c % 4) & 255; d.push((e >>> 4).toString(16)); d.push((e & 15).toString(16)) } return d.join("") }, parse: function (a) { for (var b = a.length, d = [], c = 0; c < b; c += 2) d[c >>> 3] |= parseInt(a.substr(c, 2), 16) << 24 - 4 * (c % 8); return new j.init(d, b / 2) } }, p = m.Latin1 = { stringify: function (a) { var b = a.words; a = a.sigBytes; for (var d = [], c = 0; c < a; c++) d.push(String.fromCharCode(b[c >>> 2] >>> 24 - 8 * (c % 4) & 255)); return d.join("") }, parse: function (a) { for (var b = a.length, d = [], c = 0; c < b; c++) d[c >>> 2] |= (a.charCodeAt(c) & 255) << 24 - 8 * (c % 4); return new j.init(d, b) } }, t = m.Utf8 = { stringify: function (a) { try { return decodeURIComponent(escape(p.stringify(a))) } catch (b) { throw Error("Malformed UTF-8 data"); } }, parse: function (a) { return p.parse(unescape(encodeURIComponent(a))) } }, q = l.BufferedBlockAlgorithm = f.extend({ reset: function () { this._data = new j.init; this._nDataBytes = 0 }, _append: function (a) { "string" == typeof a && (a = t.parse(a)); this._data.concat(a); this._nDataBytes += a.sigBytes }, _process: function (a) { var b = this._data, d = b.words, c = b.sigBytes, e = this.blockSize, f = c / (4 * e), f = a ? h.ceil(f) : h.max((f | 0) - this._minBufferSize, 0); a = f * e; c = h.min(4 * a, c); if (a) { for (var g = 0; g < a; g += e) this._doProcessBlock(d, g); g = d.splice(0, a); b.sigBytes -= c } return new j.init(g, c) }, clone: function () { var a = f.clone.call(this); a._data = this._data.clone(); return a }, _minBufferSize: 0 }); l.Hasher = q.extend({ cfg: f.extend(), init: function (a) { this.cfg = this.cfg.extend(a); this.reset() }, reset: function () { q.reset.call(this); this._doReset() }, update: function (a) { this._append(a); this._process(); return this }, finalize: function (a) { a && this._append(a); return this._doFinalize() }, blockSize: 16, _createHelper: function (a) { return function (b, d) { return (new a.init(d)).finalize(b) } }, _createHmacHelper: function (a) { return function (b, d) { return (new u.HMAC.init(a, d)).finalize(b) } } }); var u = k.algo = {}; return k }(Math);
-(function () { var k = CryptoJS, b = k.lib, m = b.WordArray, l = b.Hasher, d = [], b = k.algo.SHA1 = l.extend({ _doReset: function () { this._hash = new m.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520]) }, _doProcessBlock: function (n, p) { for (var a = this._hash.words, e = a[0], f = a[1], h = a[2], j = a[3], b = a[4], c = 0; 80 > c; c++) { if (16 > c) d[c] = n[p + c] | 0; else { var g = d[c - 3] ^ d[c - 8] ^ d[c - 14] ^ d[c - 16]; d[c] = g << 1 | g >>> 31 } g = (e << 5 | e >>> 27) + b + d[c]; g = 20 > c ? g + ((f & h | ~f & j) + 1518500249) : 40 > c ? g + ((f ^ h ^ j) + 1859775393) : 60 > c ? g + ((f & h | f & j | h & j) - 1894007588) : g + ((f ^ h ^ j) - 899497514); b = j; j = h; h = f << 30 | f >>> 2; f = e; e = g } a[0] = a[0] + e | 0; a[1] = a[1] + f | 0; a[2] = a[2] + h | 0; a[3] = a[3] + j | 0; a[4] = a[4] + b | 0 }, _doFinalize: function () { var b = this._data, d = b.words, a = 8 * this._nDataBytes, e = 8 * b.sigBytes; d[e >>> 5] |= 128 << 24 - e % 32; d[(e + 64 >>> 9 << 4) + 14] = Math.floor(a / 4294967296); d[(e + 64 >>> 9 << 4) + 15] = a; b.sigBytes = 4 * d.length; this._process(); return this._hash }, clone: function () { var b = l.clone.call(this); b._hash = this._hash.clone(); return b } }); k.SHA1 = l._createHelper(b); k.HmacSHA1 = l._createHmacHelper(b) })();
-(function (k) { for (var g = CryptoJS, h = g.lib, v = h.WordArray, j = h.Hasher, h = g.algo, s = [], t = [], u = function (q) { return 4294967296 * (q - (q | 0)) | 0 }, l = 2, b = 0; 64 > b;) { var d; a: { d = l; for (var w = k.sqrt(d), r = 2; r <= w; r++) if (!(d % r)) { d = !1; break a } d = !0 } d && (8 > b && (s[b] = u(k.pow(l, 0.5))), t[b] = u(k.pow(l, 1 / 3)), b++); l++ } var n = [], h = h.SHA256 = j.extend({ _doReset: function () { this._hash = new v.init(s.slice(0)) }, _doProcessBlock: function (q, h) { for (var a = this._hash.words, c = a[0], d = a[1], b = a[2], k = a[3], f = a[4], g = a[5], j = a[6], l = a[7], e = 0; 64 > e; e++) { if (16 > e) n[e] = q[h + e] | 0; else { var m = n[e - 15], p = n[e - 2]; n[e] = ((m << 25 | m >>> 7) ^ (m << 14 | m >>> 18) ^ m >>> 3) + n[e - 7] + ((p << 15 | p >>> 17) ^ (p << 13 | p >>> 19) ^ p >>> 10) + n[e - 16] } m = l + ((f << 26 | f >>> 6) ^ (f << 21 | f >>> 11) ^ (f << 7 | f >>> 25)) + (f & g ^ ~f & j) + t[e] + n[e]; p = ((c << 30 | c >>> 2) ^ (c << 19 | c >>> 13) ^ (c << 10 | c >>> 22)) + (c & d ^ c & b ^ d & b); l = j; j = g; g = f; f = k + m | 0; k = b; b = d; d = c; c = m + p | 0 } a[0] = a[0] + c | 0; a[1] = a[1] + d | 0; a[2] = a[2] + b | 0; a[3] = a[3] + k | 0; a[4] = a[4] + f | 0; a[5] = a[5] + g | 0; a[6] = a[6] + j | 0; a[7] = a[7] + l | 0 }, _doFinalize: function () { var d = this._data, b = d.words, a = 8 * this._nDataBytes, c = 8 * d.sigBytes; b[c >>> 5] |= 128 << 24 - c % 32; b[(c + 64 >>> 9 << 4) + 14] = k.floor(a / 4294967296); b[(c + 64 >>> 9 << 4) + 15] = a; d.sigBytes = 4 * b.length; this._process(); return this._hash }, clone: function () { var b = j.clone.call(this); b._hash = this._hash.clone(); return b } }); g.SHA256 = j._createHelper(h); g.HmacSHA256 = j._createHmacHelper(h) })(Math);
-(function () { var c = CryptoJS, k = c.enc.Utf8; c.algo.HMAC = c.lib.Base.extend({ init: function (a, b) { a = this._hasher = new a.init; "string" == typeof b && (b = k.parse(b)); var c = a.blockSize, e = 4 * c; b.sigBytes > e && (b = a.finalize(b)); b.clamp(); for (var f = this._oKey = b.clone(), g = this._iKey = b.clone(), h = f.words, j = g.words, d = 0; d < c; d++) h[d] ^= 1549556828, j[d] ^= 909522486; f.sigBytes = g.sigBytes = e; this.reset() }, reset: function () { var a = this._hasher; a.reset(); a.update(this._iKey) }, update: function (a) { this._hasher.update(a); return this }, finalize: function (a) { var b = this._hasher; a = b.finalize(a); b.reset(); return b.finalize(this._oKey.clone().concat(a)) } }) })();
-(function () { var h = CryptoJS, j = h.lib.WordArray; h.enc.Base64 = { stringify: function (b) { var e = b.words, f = b.sigBytes, c = this._map; b.clamp(); b = []; for (var a = 0; a < f; a += 3) for (var d = (e[a >>> 2] >>> 24 - 8 * (a % 4) & 255) << 16 | (e[a + 1 >>> 2] >>> 24 - 8 * ((a + 1) % 4) & 255) << 8 | e[a + 2 >>> 2] >>> 24 - 8 * ((a + 2) % 4) & 255, g = 0; 4 > g && a + 0.75 * g < f; g++) b.push(c.charAt(d >>> 6 * (3 - g) & 63)); if (e = c.charAt(64)) for (; b.length % 4;) b.push(e); return b.join("") }, parse: function (b) { var e = b.length, f = this._map, c = f.charAt(64); c && (c = b.indexOf(c), -1 != c && (e = c)); for (var c = [], a = 0, d = 0; d < e; d++) if (d % 4) { var g = f.indexOf(b.charAt(d - 1)) << 2 * (d % 4), h = f.indexOf(b.charAt(d)) >>> 6 - 2 * (d % 4); c[a >>> 2] |= (g | h) << 24 - 8 * (a % 4); a++ } return j.create(c, a) }, _map: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" } })();
-
-hawk.crypto.internals = CryptoJS;
-
-
-// Export if used as a module
-
-if (typeof module !== 'undefined' && module.exports) {
-    module.exports = hawk;
-}
-
-// $lab:coverage:on$
deleted file mode 100644
--- a/browser/components/loop/content/shared/libs/sjcl-dev20140604.js
+++ /dev/null
@@ -1,606 +0,0 @@
-/** @fileOverview Javascript cryptography implementation.
- *
- * Crush to remove comments, shorten variable names and
- * generally reduce transmission size.
- *
- * @author Emily Stark
- * @author Mike Hamburg
- * @author Dan Boneh
- */
-
-"use strict";
-/*jslint indent: 2, bitwise: false, nomen: false, plusplus: false, white: false, regexp: false */
-/*global document, window, escape, unescape, module, require, Uint32Array */
-
-/** @namespace The Stanford Javascript Crypto Library, top-level namespace. */
-var sjcl = {
-  /** @namespace Symmetric ciphers. */
-  cipher: {},
-
-  /** @namespace Hash functions.  Right now only SHA256 is implemented. */
-  hash: {},
-
-  /** @namespace Key exchange functions.  Right now only SRP is implemented. */
-  keyexchange: {},
-  
-  /** @namespace Block cipher modes of operation. */
-  mode: {},
-
-  /** @namespace Miscellaneous.  HMAC and PBKDF2. */
-  misc: {},
-  
-  /**
-   * @namespace Bit array encoders and decoders.
-   *
-   * @description
-   * The members of this namespace are functions which translate between
-   * SJCL's bitArrays and other objects (usually strings).  Because it
-   * isn't always clear which direction is encoding and which is decoding,
-   * the method names are "fromBits" and "toBits".
-   */
-  codec: {},
-  
-  /** @namespace Exceptions. */
-  exception: {
-    /** @constructor Ciphertext is corrupt. */
-    corrupt: function(message) {
-      this.toString = function() { return "CORRUPT: "+this.message; };
-      this.message = message;
-    },
-    
-    /** @constructor Invalid parameter. */
-    invalid: function(message) {
-      this.toString = function() { return "INVALID: "+this.message; };
-      this.message = message;
-    },
-    
-    /** @constructor Bug or missing feature in SJCL. @constructor */
-    bug: function(message) {
-      this.toString = function() { return "BUG: "+this.message; };
-      this.message = message;
-    },
-
-    /** @constructor Something isn't ready. */
-    notReady: function(message) {
-      this.toString = function() { return "NOT READY: "+this.message; };
-      this.message = message;
-    }
-  }
-};
-
-if(typeof module !== 'undefined' && module.exports){
-  module.exports = sjcl;
-}
-/** @fileOverview Arrays of bits, encoded as arrays of Numbers.
- *
- * @author Emily Stark
- * @author Mike Hamburg
- * @author Dan Boneh
- */
-
-/** @namespace Arrays of bits, encoded as arrays of Numbers.
- *
- * @description
- * <p>
- * These objects are the currency accepted by SJCL's crypto functions.
- * </p>
- *
- * <p>
- * Most of our crypto primitives operate on arrays of 4-byte words internally,
- * but many of them can take arguments that are not a multiple of 4 bytes.
- * This library encodes arrays of bits (whose size need not be a multiple of 8
- * bits) as arrays of 32-bit words.  The bits are packed, big-endian, into an
- * array of words, 32 bits at a time.  Since the words are double-precision
- * floating point numbers, they fit some extra data.  We use this (in a private,
- * possibly-changing manner) to encode the number of bits actually  present
- * in the last word of the array.
- * </p>
- *
- * <p>
- * Because bitwise ops clear this out-of-band data, these arrays can be passed
- * to ciphers like AES which want arrays of words.
- * </p>
- */
-sjcl.bitArray = {
-  /**
-   * Array slices in units of bits.
-   * @param {bitArray} a The array to slice.
-   * @param {Number} bstart The offset to the start of the slice, in bits.
-   * @param {Number} bend The offset to the end of the slice, in bits.  If this is undefined,
-   * slice until the end of the array.
-   * @return {bitArray} The requested slice.
-   */
-  bitSlice: function (a, bstart, bend) {
-    a = sjcl.bitArray._shiftRight(a.slice(bstart/32), 32 - (bstart & 31)).slice(1);
-    return (bend === undefined) ? a : sjcl.bitArray.clamp(a, bend-bstart);
-  },
-
-  /**
-   * Extract a number packed into a bit array.
-   * @param {bitArray} a The array to slice.
-   * @param {Number} bstart The offset to the start of the slice, in bits.
-   * @param {Number} length The length of the number to extract.
-   * @return {Number} The requested slice.
-   */
-  extract: function(a, bstart, blength) {
-    // FIXME: this Math.floor is not necessary at all, but for some reason
-    // seems to suppress a bug in the Chromium JIT.
-    var x, sh = Math.floor((-bstart-blength) & 31);
-    if ((bstart + blength - 1 ^ bstart) & -32) {
-      // it crosses a boundary
-      x = (a[bstart/32|0] << (32 - sh)) ^ (a[bstart/32+1|0] >>> sh);
-    } else {
-      // within a single word
-      x = a[bstart/32|0] >>> sh;
-    }
-    return x & ((1<<blength) - 1);
-  },
-
-  /**
-   * Concatenate two bit arrays.
-   * @param {bitArray} a1 The first array.
-   * @param {bitArray} a2 The second array.
-   * @return {bitArray} The concatenation of a1 and a2.
-   */
-  concat: function (a1, a2) {
-    if (a1.length === 0 || a2.length === 0) {
-      return a1.concat(a2);
-    }
-    
-    var last = a1[a1.length-1], shift = sjcl.bitArray.getPartial(last);
-    if (shift === 32) {
-      return a1.concat(a2);
-    } else {
-      return sjcl.bitArray._shiftRight(a2, shift, last|0, a1.slice(0,a1.length-1));
-    }
-  },
-
-  /**
-   * Find the length of an array of bits.
-   * @param {bitArray} a The array.
-   * @return {Number} The length of a, in bits.
-   */
-  bitLength: function (a) {
-    var l = a.length, x;
-    if (l === 0) { return 0; }
-    x = a[l - 1];
-    return (l-1) * 32 + sjcl.bitArray.getPartial(x);
-  },
-
-  /**
-   * Truncate an array.
-   * @param {bitArray} a The array.
-   * @param {Number} len The length to truncate to, in bits.
-   * @return {bitArray} A new array, truncated to len bits.
-   */
-  clamp: function (a, len) {
-    if (a.length * 32 < len) { return a; }
-    a = a.slice(0, Math.ceil(len / 32));
-    var l = a.length;
-    len = len & 31;
-    if (l > 0 && len) {
-      a[l-1] = sjcl.bitArray.partial(len, a[l-1] & 0x80000000 >> (len-1), 1);
-    }
-    return a;
-  },
-
-  /**
-   * Make a partial word for a bit array.
-   * @param {Number} len The number of bits in the word.
-   * @param {Number} x The bits.
-   * @param {Number} [0] _end Pass 1 if x has already been shifted to the high side.
-   * @return {Number} The partial word.
-   */
-  partial: function (len, x, _end) {
-    if (len === 32) { return x; }
-    return (_end ? x|0 : x << (32-len)) + len * 0x10000000000;
-  },
-
-  /**
-   * Get the number of bits used by a partial word.
-   * @param {Number} x The partial word.
-   * @return {Number} The number of bits used by the partial word.
-   */
-  getPartial: function (x) {
-    return Math.round(x/0x10000000000) || 32;
-  },
-
-  /**
-   * Compare two arrays for equality in a predictable amount of time.
-   * @param {bitArray} a The first array.
-   * @param {bitArray} b The second array.
-   * @return {boolean} true if a == b; false otherwise.
-   */
-  equal: function (a, b) {
-    if (sjcl.bitArray.bitLength(a) !== sjcl.bitArray.bitLength(b)) {
-      return false;
-    }
-    var x = 0, i;
-    for (i=0; i<a.length; i++) {
-      x |= a[i]^b[i];
-    }
-    return (x === 0);
-  },
-
-  /** Shift an array right.
-   * @param {bitArray} a The array to shift.
-   * @param {Number} shift The number of bits to shift.
-   * @param {Number} [carry=0] A byte to carry in
-   * @param {bitArray} [out=[]] An array to prepend to the output.
-   * @private
-   */
-  _shiftRight: function (a, shift, carry, out) {
-    var i, last2=0, shift2;
-    if (out === undefined) { out = []; }
-    
-    for (; shift >= 32; shift -= 32) {
-      out.push(carry);
-      carry = 0;
-    }
-    if (shift === 0) {
-      return out.concat(a);
-    }
-    
-    for (i=0; i<a.length; i++) {
-      out.push(carry | a[i]>>>shift);
-      carry = a[i] << (32-shift);
-    }
-    last2 = a.length ? a[a.length-1] : 0;
-    shift2 = sjcl.bitArray.getPartial(last2);
-    out.push(sjcl.bitArray.partial(shift+shift2 & 31, (shift + shift2 > 32) ? carry : out.pop(),1));
-    return out;
-  },
-  
-  /** xor a block of 4 words together.
-   * @private
-   */
-  _xor4: function(x,y) {
-    return [x[0]^y[0],x[1]^y[1],x[2]^y[2],x[3]^y[3]];
-  }
-};
-/** @fileOverview Bit array codec implementations.
- *
- * @author Emily Stark
- * @author Mike Hamburg
- * @author Dan Boneh
- */
- 
-/** @namespace UTF-8 strings */
-sjcl.codec.utf8String = {
-  /** Convert from a bitArray to a UTF-8 string. */
-  fromBits: function (arr) {
-    var out = "", bl = sjcl.bitArray.bitLength(arr), i, tmp;
-    for (i=0; i<bl/8; i++) {
-      if ((i&3) === 0) {
-        tmp = arr[i/4];
-      }
-      out += String.fromCharCode(tmp >>> 24);
-      tmp <<= 8;
-    }
-    return decodeURIComponent(escape(out));
-  },
-  
-  /** Convert from a UTF-8 string to a bitArray. */
-  toBits: function (str) {
-    str = unescape(encodeURIComponent(str));
-    var out = [], i, tmp=0;
-    for (i=0; i<str.length; i++) {
-      tmp = tmp << 8 | str.charCodeAt(i);
-      if ((i&3) === 3) {
-        out.push(tmp);
-        tmp = 0;
-      }
-    }
-    if (i&3) {
-      out.push(sjcl.bitArray.partial(8*(i&3), tmp));
-    }
-    return out;
-  }
-};
-/** @fileOverview Bit array codec implementations.
- *
- * @author Emily Stark
- * @author Mike Hamburg
- * @author Dan Boneh
- */
-
-/** @namespace Hexadecimal */
-sjcl.codec.hex = {
-  /** Convert from a bitArray to a hex string. */
-  fromBits: function (arr) {
-    var out = "", i;
-    for (i=0; i<arr.length; i++) {
-      out += ((arr[i]|0)+0xF00000000000).toString(16).substr(4);
-    }
-    return out.substr(0, sjcl.bitArray.bitLength(arr)/4);//.replace(/(.{8})/g, "$1 ");
-  },
-  /** Convert from a hex string to a bitArray. */
-  toBits: function (str) {
-    var i, out=[], len;
-    str = str.replace(/\s|0x/g, "");
-    len = str.length;
-    str = str + "00000000";
-    for (i=0; i<str.length; i+=8) {
-      out.push(parseInt(str.substr(i,8),16)^0);
-    }
-    return sjcl.bitArray.clamp(out, len*4);
-  }
-};
-
-/** @fileOverview Javascript SHA-256 implementation.
- *
- * An older version of this implementation is available in the public
- * domain, but this one is (c) Emily Stark, Mike Hamburg, Dan Boneh,
- * Stanford University 2008-2010 and BSD-licensed for liability
- * reasons.
- *
- * Special thanks to Aldo Cortesi for pointing out several bugs in
- * this code.
- *
- * @author Emily Stark
- * @author Mike Hamburg
- * @author Dan Boneh
- */
-
-/**
- * Context for a SHA-256 operation in progress.
- * @constructor
- * @class Secure Hash Algorithm, 256 bits.
- */
-sjcl.hash.sha256 = function (hash) {
-  if (!this._key[0]) { this._precompute(); }
-  if (hash) {
-    this._h = hash._h.slice(0);
-    this._buffer = hash._buffer.slice(0);
-    this._length = hash._length;
-  } else {
-    this.reset();
-  }
-};
-
-/**
- * Hash a string or an array of words.
- * @static
- * @param {bitArray|String} data the data to hash.
- * @return {bitArray} The hash value, an array of 16 big-endian words.
- */
-sjcl.hash.sha256.hash = function (data) {
-  return (new sjcl.hash.sha256()).update(data).finalize();
-};
-
-sjcl.hash.sha256.prototype = {
-  /**
-   * The hash's block size, in bits.
-   * @constant
-   */
-  blockSize: 512,
-   
-  /**
-   * Reset the hash state.
-   * @return this
-   */
-  reset:function () {
-    this._h = this._init.slice(0);
-    this._buffer = [];
-    this._length = 0;
-    return this;
-  },
-  
-  /**
-   * Input several words to the hash.
-   * @param {bitArray|String} data the data to hash.
-   * @return this
-   */
-  update: function (data) {
-    if (typeof data === "string") {
-      data = sjcl.codec.utf8String.toBits(data);
-    }
-    var i, b = this._buffer = sjcl.bitArray.concat(this._buffer, data),
-        ol = this._length,
-        nl = this._length = ol + sjcl.bitArray.bitLength(data);
-    for (i = 512+ol & -512; i <= nl; i+= 512) {
-      this._block(b.splice(0,16));
-    }
-    return this;
-  },
-  
-  /**
-   * Complete hashing and output the hash value.
-   * @return {bitArray} The hash value, an array of 8 big-endian words.
-   */
-  finalize:function () {
-    var i, b = this._buffer, h = this._h;
-
-    // Round out and push the buffer
-    b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1,1)]);
-    
-    // Round out the buffer to a multiple of 16 words, less the 2 length words.
-    for (i = b.length + 2; i & 15; i++) {
-      b.push(0);
-    }
-    
-    // append the length
-    b.push(Math.floor(this._length / 0x100000000));
-    b.push(this._length | 0);
-
-    while (b.length) {
-      this._block(b.splice(0,16));
-    }
-
-    this.reset();
-    return h;
-  },
-
-  /**
-   * The SHA-256 initialization vector, to be precomputed.
-   * @private
-   */
-  _init:[],
-  /*
-  _init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],
-  */
-  
-  /**
-   * The SHA-256 hash key, to be precomputed.
-   * @private
-   */
-  _key:[],
-  /*
-  _key:
-    [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
-     0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
-     0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
-     0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
-     0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
-     0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
-     0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
-     0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2],
-  */
-
-
-  /**
-   * Function to precompute _init and _key.
-   * @private
-   */
-  _precompute: function () {
-    var i = 0, prime = 2, factor;
-
-    function frac(x) { return (x-Math.floor(x)) * 0x100000000 | 0; }
-
-    outer: for (; i<64; prime++) {
-      for (factor=2; factor*factor <= prime; factor++) {
-        if (prime % factor === 0) {
-          // not a prime
-          continue outer;
-        }
-      }
-      
-      if (i<8) {
-        this._init[i] = frac(Math.pow(prime, 1/2));
-      }
-      this._key[i] = frac(Math.pow(prime, 1/3));
-      i++;
-    }
-  },
-  
-  /**
-   * Perform one cycle of SHA-256.
-   * @param {bitArray} words one block of words.
-   * @private
-   */
-  _block:function (words) {  
-    var i, tmp, a, b,
-      w = words.slice(0),
-      h = this._h,
-      k = this._key,
-      h0 = h[0], h1 = h[1], h2 = h[2], h3 = h[3],
-      h4 = h[4], h5 = h[5], h6 = h[6], h7 = h[7];
-
-    /* Rationale for placement of |0 :
-     * If a value can overflow is original 32 bits by a factor of more than a few
-     * million (2^23 ish), there is a possibility that it might overflow the
-     * 53-bit mantissa and lose precision.
-     *
-     * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that
-     * propagates around the loop, and on the hash state h[].  I don't believe
-     * that the clamps on h4 and on h0 are strictly necessary, but it's close
-     * (for h4 anyway), and better safe than sorry.
-     *
-     * The clamps on h[] are necessary for the output to be correct even in the
-     * common case and for short inputs.
-     */
-    for (i=0; i<64; i++) {
-      // load up the input word for this round
-      if (i<16) {
-        tmp = w[i];
-      } else {
-        a   = w[(i+1 ) & 15];
-        b   = w[(i+14) & 15];
-        tmp = w[i&15] = ((a>>>7  ^ a>>>18 ^ a>>>3  ^ a<<25 ^ a<<14) + 
-                         (b>>>17 ^ b>>>19 ^ b>>>10 ^ b<<15 ^ b<<13) +
-                         w[i&15] + w[(i+9) & 15]) | 0;
-      }
-      
-      tmp = (tmp + h7 + (h4>>>6 ^ h4>>>11 ^ h4>>>25 ^ h4<<26 ^ h4<<21 ^ h4<<7) +  (h6 ^ h4&(h5^h6)) + k[i]); // | 0;
-      
-      // shift register
-      h7 = h6; h6 = h5; h5 = h4;
-      h4 = h3 + tmp | 0;
-      h3 = h2; h2 = h1; h1 = h0;
-
-      h0 = (tmp +  ((h1&h2) ^ (h3&(h1^h2))) + (h1>>>2 ^ h1>>>13 ^ h1>>>22 ^ h1<<30 ^ h1<<19 ^ h1<<10)) | 0;
-    }
-
-    h[0] = h[0]+h0 | 0;
-    h[1] = h[1]+h1 | 0;
-    h[2] = h[2]+h2 | 0;
-    h[3] = h[3]+h3 | 0;
-    h[4] = h[4]+h4 | 0;
-    h[5] = h[5]+h5 | 0;
-    h[6] = h[6]+h6 | 0;
-    h[7] = h[7]+h7 | 0;
-  }
-};
-
-
-/** @fileOverview HMAC implementation.
- *
- * @author Emily Stark
- * @author Mike Hamburg
- * @author Dan Boneh
- */
-
-/** HMAC with the specified hash function.
- * @constructor
- * @param {bitArray} key the key for HMAC.
- * @param {Object} [hash=sjcl.hash.sha256] The hash function to use.
- */
-sjcl.misc.hmac = function (key, Hash) {
-  this._hash = Hash = Hash || sjcl.hash.sha256;
-  var exKey = [[],[]], i,
-      bs = Hash.prototype.blockSize / 32;
-  this._baseHash = [new Hash(), new Hash()];
-
-  if (key.length > bs) {
-    key = Hash.hash(key);
-  }
-  
-  for (i=0; i<bs; i++) {
-    exKey[0][i] = key[i]^0x36363636;
-    exKey[1][i] = key[i]^0x5C5C5C5C;
-  }
-  
-  this._baseHash[0].update(exKey[0]);
-  this._baseHash[1].update(exKey[1]);
-  this._resultHash = new Hash(this._baseHash[0]);
-};
-
-/** HMAC with the specified hash function.  Also called encrypt since it's a prf.
- * @param {bitArray|String} data The data to mac.
- */
-sjcl.misc.hmac.prototype.encrypt = sjcl.misc.hmac.prototype.mac = function (data) {
-  if (!this._updated) {
-    this.update(data);
-    return this.digest(data);
-  } else {
-    throw new sjcl.exception.invalid("encrypt on already updated hmac called!");
-  }
-};
-
-sjcl.misc.hmac.prototype.reset = function () {
-  this._resultHash = new this._hash(this._baseHash[0]);
-  this._updated = false;
-};
-
-sjcl.misc.hmac.prototype.update = function (data) {
-  this._updated = true;
-  this._resultHash.update(data);
-};
-
-sjcl.misc.hmac.prototype.digest = function () {
-  var w = this._resultHash.finalize(), result = new (this._hash)(this._baseHash[1]).update(w).finalize();
-
-  this.reset();
-
-  return result;
-};
\ No newline at end of file
deleted file mode 100644
--- a/browser/components/loop/content/shared/libs/token.js
+++ /dev/null
@@ -1,78 +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/. */
-
-'use strict';
- 
-var PREFIX_NAME = 'identity.mozilla.com/picl/v1/';
-var bitSlice = sjcl.bitArray.bitSlice;
-var salt = sjcl.codec.hex.toBits('');
-
-/**
- * hkdf - The HMAC-based Key Derivation Function
- * based on https://github.com/mozilla/node-hkdf
- *
- * @class hkdf
- * @param {bitArray} ikm Initial keying material
- * @param {bitArray} info Key derivation data
- * @param {bitArray} salt Salt
- * @param {integer} length Length of the derived key in bytes
- * @return promise object- It will resolve with `output` data
- */
-function hkdf(ikm, info, salt, length, callback) {
-  var mac = new sjcl.misc.hmac(salt, sjcl.hash.sha256);
-  mac.update(ikm);
-
-  // compute the PRK
-  var prk = mac.digest();
-
-  // hash length is 32 because only sjcl.hash.sha256 is used at this moment
-  var hashLength = 32;
-  var num_blocks = Math.ceil(length / hashLength);
-  var prev = sjcl.codec.hex.toBits('');
-  var output = '';
-
-  for (var i = 0; i < num_blocks; i++) {
-    var hmac = new sjcl.misc.hmac(prk, sjcl.hash.sha256);
-
-    var input = sjcl.bitArray.concat(
-      sjcl.bitArray.concat(prev, info),
-      sjcl.codec.utf8String.toBits((String.fromCharCode(i + 1)))
-    );
-
-    hmac.update(input);
-
-    prev = hmac.digest();
-    output += sjcl.codec.hex.fromBits(prev);
-  }
-
-  var truncated = sjcl.bitArray.clamp(sjcl.codec.hex.toBits(output), length * 8);
-
-  callback(truncated);
-}
-
- 
-/**
-* @class hawkCredentials
-* @method deriveHawkCredentials
-* @param {String} tokenHex
-* @param {String} context
-* @param {int} size
-* @returns {Promise}
-*/
-function deriveHawkCredentials(tokenHex, context, size, callback) {
-  var token = sjcl.codec.hex.toBits(tokenHex);
-  var info = sjcl.codec.utf8String.toBits(PREFIX_NAME + context);
-
-  hkdf(token, info, salt, size || 3 * 32, function(out) {
-    var authKey = bitSlice(out, 8 * 32, 8 * 64);
-    var bundleKey = bitSlice(out, 8 * 64);
-    callback({
-      algorithm: 'sha256',
-      id: sjcl.codec.hex.fromBits(bitSlice(out, 0, 8 * 32)),
-      key: sjcl.codec.hex.fromBits(authKey),
-      bundleKey: bundleKey
-    });
-  });
-}
- 
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -6,30 +6,27 @@ browser.jar:
   content/browser/loop/conversation.html             (content/conversation.html)
   content/browser/loop/panel.html                    (content/panel.html)
   content/browser/loop/shared/css/common.css         (content/shared/css/common.css)
   content/browser/loop/shared/css/panel.css          (content/shared/css/panel.css)
   content/browser/loop/shared/css/conversation.css   (content/shared/css/conversation.css)
   content/browser/loop/shared/img/icon_32.png        (content/shared/img/icon_32.png)
   content/browser/loop/shared/img/icon_64.png        (content/shared/img/icon_64.png)
   content/browser/loop/shared/img/loading-icon.gif   (content/shared/img/loading-icon.gif)
-  content/browser/loop/shared/js/client.js           (content/shared/js/client.js)
   content/browser/loop/shared/js/models.js           (content/shared/js/models.js)
   content/browser/loop/shared/js/router.js           (content/shared/js/router.js)
   content/browser/loop/shared/js/views.js            (content/shared/js/views.js)
   content/browser/loop/shared/libs/lodash-2.4.1.js   (content/shared/libs/lodash-2.4.1.js)
   content/browser/loop/shared/libs/jquery-2.1.0.js   (content/shared/libs/jquery-2.1.0.js)
   content/browser/loop/shared/libs/backbone-1.1.2.js (content/shared/libs/backbone-1.1.2.js)
-  content/browser/loop/shared/libs/sjcl-dev20140604.js        (content/shared/libs/sjcl-dev20140604.js)
-  content/browser/loop/shared/libs/token.js          (content/shared/libs/token.js)
-  content/browser/loop/shared/libs/hawk-browser-2.2.1.js      (content/shared/libs/hawk-browser-2.2.1.js)
   content/browser/loop/shared/sounds/Firefox-Long.ogg   (content/shared/sounds/Firefox-Long.ogg)
   content/browser/loop/libs/l10n.js                  (content/libs/l10n.js)
+  content/browser/loop/js/client.js                  (content/js/client.js)
+  content/browser/loop/js/conversation.js            (content/js/conversation.js)
   content/browser/loop/js/desktopRouter.js           (content/js/desktopRouter.js)
-  content/browser/loop/js/conversation.js            (content/js/conversation.js)
   content/browser/loop/js/panel.js                   (content/js/panel.js)
   # Partner SDK assets
   content/browser/loop/libs/sdk.js                                             (content/libs/sdk.js)
   content/browser/loop/otcdn/webrtc/v2.2.5/css/ot.min.css                             (content/libs/otcdn/webrtc/v2.2.5/css/ot.min.css)
   content/browser/loop/otcdn/webrtc/v2.2.5/js/dynamic_config.min.js                   (content/libs/otcdn/webrtc/v2.2.5/js/dynamic_config.min.js)
   content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-chrome.png        (content/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-chrome.png)
   content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-copy-firefox.png  (content/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-copy-firefox.png)
   content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-firefox.png       (content/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-firefox.png)
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -25,20 +25,20 @@
     <script src="https://static.opentok.com/webrtc/v2.2/js/opentok.min.js"></script>
     <script type="text/javascript" src="libs/webl10n-20130617.js"></script>
     <script type="text/javascript" src="shared/libs/jquery-2.1.0.js"></script>
     <script type="text/javascript" src="shared/libs/lodash-2.4.1.js"></script>
     <script type="text/javascript" src="shared/libs/backbone-1.1.2.js"></script>
 
     <!-- app scripts -->
     <script type="text/javascript" src="config.js"></script>
-    <script type="text/javascript" src="shared/js/client.js"></script>
     <script type="text/javascript" src="shared/js/models.js"></script>
     <script type="text/javascript" src="shared/js/views.js"></script>
     <script type="text/javascript" src="shared/js/router.js"></script>
+    <script type="text/javascript" src="js/standaloneClient.js"></script>
     <script type="text/javascript" src="js/webapp.js"></script>
 
     <script>
     // Set the 'lang' and 'dir' attributes to <html> when the page is translated
     window.addEventListener('localized', function() {
       document.documentElement.lang = document.webL10n.getLanguage();
       document.documentElement.dir = document.webL10n.getDirection();
     }, false);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/standalone/content/js/standaloneClient.js
@@ -0,0 +1,123 @@
+/* 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/. */
+
+/* global loop:true, hawk, deriveHawkCredentials */
+
+var loop = loop || {};
+loop.StandaloneClient = (function($) {
+  "use strict";
+
+  // The expected properties to be returned from the POST /calls request.
+  var expectedCallsProperties = [ "sessionId", "sessionToken", "apiKey" ];
+
+  /**
+   * Loop server standalone client.
+   *
+   * @param {Object} settings Settings object.
+   */
+  function StandaloneClient(settings) {
+    settings = settings || {};
+    if (!settings.baseServerUrl) {
+      throw new Error("missing required baseServerUrl");
+    }
+
+    this.settings = settings;
+  }
+
+  StandaloneClient.prototype = {
+    /**
+     * Validates a data object to confirm it has the specified properties.
+     *
+     * @param  {Object} The data object to verify
+     * @param  {Array} The list of properties to verify within the object
+     * @return This returns either the specific property if only one
+     *         property is specified, or it returns all properties
+     */
+    _validate: function(data, properties) {
+      if (typeof data !== "object") {
+        throw new Error("Invalid data received from server");
+      }
+
+      properties.forEach(function (property) {
+        if (!data.hasOwnProperty(property)) {
+          throw new Error("Invalid data received from server - missing " +
+            property);
+        }
+      });
+
+      if (properties.length == 1) {
+        return data[properties[0]];
+      }
+
+      return data;
+    },
+
+    /**
+     * Generic handler for XHR failures.
+     *
+     * @param {Function} cb Callback(err)
+     * @param jqXHR See jQuery docs
+     * @param textStatus See jQuery docs
+     * @param errorThrown See jQuery docs
+     */
+    _failureHandler: function(cb, jqXHR, textStatus, errorThrown) {
+      var error = "Unknown error.",
+          jsonRes = jqXHR && jqXHR.responseJSON || {};
+      // Received error response format:
+      // { "status": "errors",
+      //   "errors": [{
+      //      "location": "url",
+      //      "name": "token",
+      //      "description": "invalid token"
+      // }]}
+      if (jsonRes.status === "errors" && Array.isArray(jsonRes.errors)) {
+        error = "Details: " + jsonRes.errors.map(function(err) {
+          return Object.keys(err).map(function(field) {
+            return field + ": " + err[field];
+          }).join(", ");
+        }).join("; ");
+      }
+      var message = "HTTP " + jqXHR.status + " " + errorThrown +
+          "; " + error;
+      console.error(message);
+      cb(new Error(message));
+    },
+
+    /**
+     * Posts a call request to the server for a call represented by the
+     * loopToken. Will return the session data for the call.
+     *
+     * @param  {String} loopToken The loopToken representing the call
+     * @param  {String} callType The type of media in the call, e.g.
+     *                           "audio" or "audio-video"
+     * @param  {Function} cb Callback(err, sessionData)
+     */
+    requestCallInfo: function(loopToken, callType, cb) {
+      if (!loopToken) {
+        throw new Error("missing required parameter loopToken");
+      }
+
+      var req = $.ajax({
+        url:         this.settings.baseServerUrl + "/calls/" + loopToken,
+        method:      "POST",
+        contentType: "application/json",
+        dataType:    "json",
+        data: JSON.stringify({callType: callType})
+      });
+
+      req.done(function(sessionData) {
+        try {
+          cb(null, this._validate(sessionData, expectedCallsProperties));
+        } catch (err) {
+          console.log("Error requesting call info", err);
+          cb(err);
+        }
+      }.bind(this));
+
+      req.fail(this._failureHandler.bind(this, cb));
+    },
+  };
+
+  return StandaloneClient;
+})(jQuery);
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -87,18 +87,23 @@ loop.webapp = (function($, _, OT) {
     /**
      * Initiates the call.
      *
      * @param {SubmitEvent} event
      */
     initiate: function(event) {
       event.preventDefault();
       this.model.initiate({
-        baseServerUrl: baseServerUrl,
-        outgoing: true
+        client: new loop.StandaloneClient({
+          baseServerUrl: baseServerUrl,
+        }),
+        outgoing: true,
+        // For now, we assume both audio and video as there is no
+        // other option to select.
+        callType: "audio-video"
       });
       this.disableForm();
     }
   });
 
   /**
    * Webapp Router.
    */
rename from browser/components/loop/test/shared/client_test.js
rename to browser/components/loop/test/desktop-local/client_test.js
--- a/browser/components/loop/test/shared/client_test.js
+++ b/browser/components/loop/test/desktop-local/client_test.js
@@ -1,291 +1,184 @@
 /* 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/. */
 
 /*global loop, sinon, it, beforeEach, afterEach, describe, hawk */
 
 var expect = chai.expect;
 
-describe("loop.shared.Client", function() {
+describe("loop.Client", function() {
   "use strict";
 
   var sandbox,
-      fakeXHR,
-      requests = [],
       callback,
+      client,
       mozLoop,
-      fakeToken;
+      fakeToken,
+      hawkRequestStub;
 
-  var fakeErrorRes = JSON.stringify({
-      status: "errors",
-      errors: [{
-        location: "url",
-        name: "token",
-        description: "invalid token"
-      }]
-    });
+  var fakeErrorRes = {
+      code: 400,
+      errno: 400,
+      error: "Request Failed",
+      message: "invalid token"
+    };
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
-    fakeXHR = sandbox.useFakeXMLHttpRequest();
-    requests = [];
-    // https://github.com/cjohansen/Sinon.JS/issues/393
-    fakeXHR.xhr.onCreate = function (xhr) {
-      requests.push(xhr);
-    };
     callback = sinon.spy();
     fakeToken = "fakeTokenText";
+    mozLoop = {
+      getLoopCharPref: sandbox.stub()
+        .returns(null)
+        .withArgs("hawk-session-token")
+        .returns(fakeToken),
+      ensureRegistered: sinon.stub().callsArgWith(0, null),
+      noteCallUrlExpiry: sinon.spy(),
+      hawkRequest: sinon.stub()
+    };
+    // Alias for clearer tests.
+    hawkRequestStub = mozLoop.hawkRequest;
+    client = new loop.Client({
+      mozLoop: mozLoop
+    });
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
-  describe("loop.shared.Client", function() {
-    describe("#constructor", function() {
-      it("should require a baseServerUrl setting", function() {
-        expect(function() {
-          new loop.shared.Client();
-        }).to.Throw(Error, /required/);
-      });
-    });
-
+  describe("loop.Client", function() {
     describe("#requestCallUrl", function() {
-      var client;
-
-      beforeEach(function() {
-        window.navigator.mozLoop = {
-          ensureRegistered: sinon.stub().callsArgWith(0, null),
-          noteCallUrlExpiry: sinon.spy(),
-          getLoopCharPref: sandbox.stub()
-            .returns(null)
-            .withArgs("hawk-session-token")
-            .returns(fakeToken)
-        };
-        client = new loop.shared.Client(
-          {baseServerUrl: "http://fake.api", mozLoop: window.navigator.mozLoop}
-        );
-      });
-
       it("should ensure loop is registered", function() {
         client.requestCallUrl("foo", callback);
 
-        sinon.assert.calledOnce(navigator.mozLoop.ensureRegistered);
+        sinon.assert.calledOnce(mozLoop.ensureRegistered);
       });
 
       it("should send an error when registration fails", function() {
-        navigator.mozLoop.ensureRegistered.callsArgWith(0, "offline");
+        mozLoop.ensureRegistered.callsArgWith(0, "offline");
 
         client.requestCallUrl("foo", callback);
 
         sinon.assert.calledOnce(callback);
         sinon.assert.calledWithExactly(callback, "offline");
       });
 
       it("should post to /call-url/", function() {
         client.requestCallUrl("foo", callback);
 
-        expect(requests).to.have.length.of(1);
-        expect(requests[0].method).to.be.equal("POST");
-        expect(requests[0].url).to.be.equal("http://fake.api/call-url/");
-        expect(requests[0].requestBody).to.be.equal('callerId=foo');
+        sinon.assert.calledOnce(hawkRequestStub);
+        sinon.assert.calledWith(hawkRequestStub,
+                                "/call-url/", "POST", {callerId: "foo"});
       });
 
-      it("should set the XHR Authorization header", function() {
-        sandbox.stub(hawk.client, "header").returns( {field: fakeToken} );
-        client._credentials = {
-          // XXX we probably really want to stub out external module calls
-          // eg deriveHawkCredentials, rather supplying them with valid arguments
-          // like we're doing here:
-          key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
-          algorithm: 'sha256',
-          user: 'Steve'
-        };
-
-        client.requestCallUrl("foo", callback);
-
-        expect(requests[0].requestHeaders.Authorization).to.equal(fakeToken);
-      });
-
-      it("should request a call url", function() {
+      it("should call the callback with the url when the request succeeds", function() {
         var callUrlData = {
-          "call_url": "fakeCallUrl",
+          "callUrl": "fakeCallUrl",
           "expiresAt": 60
         };
 
+        // Sets up the hawkRequest stub to trigger the callback with no error
+        // and the url.
+        hawkRequestStub.callsArgWith(3, null,
+                                     JSON.stringify(callUrlData));
+
         client.requestCallUrl("foo", callback);
-        requests[0].respond(200, {"Content-Type": "application/json"},
-                            JSON.stringify(callUrlData));
 
         sinon.assert.calledWithExactly(callback, null, callUrlData);
       });
 
-      it("should note the call url expiry", function() {
+      it("should note the call url expiry when the request succeeds", function() {
         var callUrlData = {
-          "call_url": "fakeCallUrl",
+          "callUrl": "fakeCallUrl",
           "expiresAt": 60
         };
 
+        // Sets up the hawkRequest stub to trigger the callback with no error
+        // and the url.
+        hawkRequestStub.callsArgWith(3, null,
+                                     JSON.stringify(callUrlData));
+
         client.requestCallUrl("foo", callback);
-        requests[0].respond(200, {"Content-Type": "application/json"},
-                            JSON.stringify(callUrlData));
 
         // expiresAt is in hours, and noteCallUrlExpiry wants seconds.
-        sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
+        sinon.assert.calledOnce(mozLoop.noteCallUrlExpiry);
+        sinon.assert.calledWithExactly(mozLoop.noteCallUrlExpiry,
           60 * 60 * 60);
       });
 
       it("should send an error when the request fails", function() {
+        // Sets up the hawkRequest stub to trigger the callback with
+        // an error
+        hawkRequestStub.callsArgWith(3, fakeErrorRes);
+
         client.requestCallUrl("foo", callback);
 
-        expect(requests).to.have.length.of(1);
-        requests[0].respond(400, {"Content-Type": "application/json"},
-                            fakeErrorRes);
-
+        sinon.assert.calledOnce(callback);
         sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
           return /400.*invalid token/.test(err.message);
         }));
       });
 
       it("should send an error if the data is not valid", function() {
+        // Sets up the hawkRequest stub to trigger the callback with
+        // an error
+        hawkRequestStub.callsArgWith(3, null, "{}");
+
         client.requestCallUrl("foo", callback);
-        requests[0].respond(200, {"Content-Type": "application/json"},
-                            '{"bad": {}}');
 
+        sinon.assert.calledOnce(callback);
         sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
           return /Invalid data received/.test(err.message);
         }));
       });
     });
 
     describe("#requestCallsInfo", function() {
-      var client;
-
-      beforeEach(function() {
-        mozLoop = {
-          getLoopCharPref: sandbox.stub()
-            .returns(null)
-            .withArgs("hawk-session-token")
-            .returns(fakeToken)
-        };
-        client = new loop.shared.Client(
-          {baseServerUrl: "http://fake.api", mozLoop: mozLoop}
-        );
-      });
-
       it("should prevent launching a conversation when version is missing",
         function() {
           expect(function() {
             client.requestCallsInfo();
           }).to.Throw(Error, /missing required parameter version/);
         });
 
-      it("should request data for all calls", function() {
+      it("should perform a get on /calls", function() {
         client.requestCallsInfo(42, callback);
 
-        expect(requests).to.have.length.of(1);
-        expect(requests[0].url).to.be.equal("http://fake.api/calls?version=42");
-        expect(requests[0].method).to.be.equal("GET");
+        sinon.assert.calledOnce(hawkRequestStub);
+        sinon.assert.calledWith(hawkRequestStub,
+                                "/calls?version=42", "GET", null);
+
+      });
 
-        requests[0].respond(200, {"Content-Type": "application/json"},
-                                 '{"calls": [{"apiKey": "fake"}]}');
+      it("should request data for all calls", function() {
+        hawkRequestStub.callsArgWith(3, null,
+                                     '{"calls": [{"apiKey": "fake"}]}');
+
+        client.requestCallsInfo(42, callback);
+
         sinon.assert.calledWithExactly(callback, null, [{apiKey: "fake"}]);
       });
 
-      it("should set the XHR Authorization header", function() {
-        sandbox.stub(hawk.client, "header").returns( {field: fakeToken} );
-        // XXX we probably really want to stub out external module calls
-        // eg deriveHawkCredentials, rather supplying them with valid arguments
-        // like we're doing here:
-        client._credentials = {
-          key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
-          algorithm: 'sha256',
-          user: 'Steve'
-        };
+      it("should send an error when the request fails", function() {
+        hawkRequestStub.callsArgWith(3, fakeErrorRes);
 
-        client.requestCallsInfo("foo", callback);
-
-        expect(requests[0].requestHeaders.Authorization).to.equal(fakeToken);
-      });
-
-      it("should send an error when the request fails", function() {
         client.requestCallsInfo(42, callback);
 
-        requests[0].respond(400, {"Content-Type": "application/json"},
-                                 fakeErrorRes);
         sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
           return /400.*invalid token/.test(err.message);
         }));
       });
 
       it("should send an error if the data is not valid", function() {
+        hawkRequestStub.callsArgWith(3, null, "{}");
+
         client.requestCallsInfo(42, callback);
 
-        requests[0].respond(200, {"Content-Type": "application/json"},
-                                 '{"bad": {}}');
-        sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
-          return /Invalid data received/.test(err.message);
-        }));
-      });
-    });
-
-    describe("requestCallInfo", function() {
-      var client;
-
-      beforeEach(function() {
-        client = new loop.shared.Client(
-          {baseServerUrl: "http://fake.api", mozLoop: undefined}
-        );
-      });
-
-      it("should prevent launching a conversation when token is missing",
-        function() {
-          expect(function() {
-            client.requestCallInfo();
-          }).to.Throw(Error, /missing.*[Tt]oken/);
-        });
-
-      it("should post data for the given call", function() {
-        client.requestCallInfo("fake", callback);
-
-        expect(requests).to.have.length.of(1);
-        expect(requests[0].url).to.be.equal("http://fake.api/calls/fake");
-        expect(requests[0].method).to.be.equal("POST");
-      });
-
-      it("should receive call data for the given call", function() {
-        client.requestCallInfo("fake", callback);
-
-        var sessionData = {
-          sessionId: "one",
-          sessionToken: "two",
-          apiKey: "three"
-        };
-
-        requests[0].respond(200, {"Content-Type": "application/json"},
-                            JSON.stringify(sessionData));
-        sinon.assert.calledWithExactly(callback, null, sessionData);
-      });
-
-      it("should send an error when the request fails", function() {
-        client.requestCallInfo("fake", callback);
-
-        requests[0].respond(400, {"Content-Type": "application/json"},
-                            fakeErrorRes);
-        sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
-          return /400.*invalid token/.test(err.message);
-        }));
-      });
-
-      it("should send an error if the data is not valid", function() {
-        client.requestCallInfo("fake", callback);
-
-        requests[0].respond(200, {"Content-Type": "application/json"},
-                            '{"bad": "one"}');
         sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
           return /Invalid data received/.test(err.message);
         }));
       });
     });
   });
 });
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -26,25 +26,26 @@
   <script src="../shared/vendor/sinon-1.9.0.js"></script>
   <script>
     /*global chai,mocha */
     chai.Assertion.includeStack = true;
     mocha.setup('bdd');
   </script>
 
   <!-- App scripts -->
-  <script src="../../content/shared/js/client.js"></script>
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/router.js"></script>
   <script src="../../content/shared/js/views.js"></script>
+  <script src="../../content/js/client.js"></script>
+  <script src="../../content/js/conversation.js"></script>
   <script src="../../content/js/desktopRouter.js"></script>
-  <script src="../../content/js/conversation.js"></script>
   <script src="../../content/js/panel.js"></script>
 
   <!-- Test scripts -->
+  <script src="client_test.js"></script>
   <script src="conversation_test.js"></script>
   <script src="panel_test.js"></script>
   <script>
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
   </script>
 </body>
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -4,17 +4,18 @@
 
 /*global loop, sinon */
 
 var expect = chai.expect;
 
 describe("loop.panel", function() {
   "use strict";
 
-  var sandbox, notifier, fakeXHR, requests = [], savedMozLoop;
+  var sandbox, notifier, fakeXHR, requests = [], savedMozLoop,
+      fakeSeenToSPref = 0;
 
   function createTestRouter(fakeDocument) {
     return new loop.panel.PanelRouter({
       notifier: notifier,
       document: fakeDocument
     });
   }
 
@@ -40,18 +41,26 @@ describe("loop.panel", function() {
       get serverUrl() {
         return "http://example.com";
       },
       getStrings: function() {
         return "{}";
       },
       get locale() {
         return "en-US";
+      },
+      setLoopCharPref: sandbox.stub(),
+      getLoopCharPref: function () {
+        if (fakeSeenToSPref === 0) {
+          return null;
+        }
+        return 'seen';
       }
     };
+
     document.mozL10n.initialize(navigator.mozLoop);
   });
 
   afterEach(function() {
     delete navigator.mozLoop;
     $("#fixtures").empty();
     sandbox.restore();
   });
@@ -205,67 +214,67 @@ describe("loop.panel", function() {
 
   describe("loop.panel.PanelView", function() {
     beforeEach(function() {
       $("#fixtures").append('<div id="messages"></div><div id="main"></div>');
     });
 
     describe("#getCallUrl", function() {
       it("should reset all pending notifications", function() {
-        var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
+        var requestCallUrl = sandbox.stub(loop.Client.prototype,
                                           "requestCallUrl");
         var view = new loop.panel.PanelView({notifier: notifier}).render();
 
         view.getCallUrl({preventDefault: sandbox.spy()});
 
         sinon.assert.calledOnce(view.notifier.clear, "clear");
       });
 
       it("should request a call url to the server", function() {
-        var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
+        var requestCallUrl = sandbox.stub(loop.Client.prototype,
                                           "requestCallUrl");
         var view = new loop.panel.PanelView({notifier: notifier});
         sandbox.stub(view, "getNickname").returns("foo");
 
         view.getCallUrl({preventDefault: sandbox.spy()});
 
         sinon.assert.calledOnce(requestCallUrl);
         sinon.assert.calledWith(requestCallUrl, "foo");
       });
 
       it("should set the call url form in a pending state", function() {
-        var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
+        var requestCallUrl = sandbox.stub(loop.Client.prototype,
                                           "requestCallUrl");
         sandbox.stub(loop.panel.PanelView.prototype, "setPending");
 
         var view = new loop.panel.PanelView({notifier: notifier});
 
         view.getCallUrl({preventDefault: sandbox.spy()});
 
         sinon.assert.calledOnce(view.setPending);
       });
 
       it("should clear the pending state when a response is received",
         function() {
           sandbox.stub(loop.panel.PanelView.prototype,
                        "clearPending");
           var requestCallUrl = sandbox.stub(
-            loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
+            loop.Client.prototype, "requestCallUrl", function(_, cb) {
               cb("fake error");
             });
           var view = new loop.panel.PanelView({notifier: notifier});
 
           view.getCallUrl({preventDefault: sandbox.spy()});
 
           sinon.assert.calledOnce(view.clearPending);
         });
 
       it("should notify the user when the operation failed", function() {
         var requestCallUrl = sandbox.stub(
-          loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
+          loop.Client.prototype, "requestCallUrl", function(_, cb) {
             cb("fake error");
           });
         var view = new loop.panel.PanelView({notifier: notifier});
 
         view.getCallUrl({preventDefault: sandbox.spy()});
 
         sinon.assert.calledOnce(view.notifier.errorL10n);
         sinon.assert.calledWithExactly(view.notifier.errorL10n,
@@ -273,17 +282,17 @@ describe("loop.panel", function() {
       });
     });
 
     describe("#onCallUrlReceived", function() {
       var callUrlData;
 
       beforeEach(function() {
         callUrlData = {
-          call_url: "http://call.me/",
+          callUrl: "http://call.me/",
           expiresAt: 1000
         };
       });
 
       it("should update the text field with the call url", function() {
         var view = new loop.panel.PanelView({notifier: notifier});
         view.render();
 
@@ -317,11 +326,63 @@ describe("loop.panel", function() {
         var renderDnD = sandbox.stub(loop.panel.DoNotDisturbView.prototype,
                                      "render");
         var view = new loop.panel.PanelView({notifier: notifier});
 
         view.render();
 
         sinon.assert.calledOnce(renderDnD);
       });
+
+      it("should render a ToSView", function() {
+        var renderToS = sandbox.stub(loop.panel.ToSView.prototype, "render");
+        var view = new loop.panel.PanelView({notifier: notifier});
+
+        view.render();
+
+        sinon.assert.calledOnce(renderToS);
+      });
+    });
+
+    describe('loop.panel.ToSView', function() {
+
+      beforeEach(function() {
+
+        $('#fixtures').append('<div id="#tos-view"></div>');
+
+      });
+
+      // XXX Until it's possible to easily test creation of text,
+      // not doing so. As it stands, the magic in the L10nView
+      // class makes stubbing BaseView.render impractical.
+
+      it("should set the value of the loop.seenToS preference to 'seen'",
+        function() {
+          var ToSView = new loop.panel.ToSView({el: $("#tos-view")});
+
+          ToSView.render();
+
+          sinon.assert.calledOnce(navigator.mozLoop.setLoopCharPref);
+          sinon.assert.calledWithExactly(navigator.mozLoop.setLoopCharPref,
+            'seenToS', 'seen');
+        });
+
+      it("should render when the value of loop.seenToS is not set", function() {
+        var renderToS = sandbox.spy(loop.panel.ToSView.prototype, "render");
+        var ToSView = new loop.panel.ToSView({el: $('#tos-view')});
+
+        ToSView.render();
+
+        sinon.assert.calledOnce(renderToS);
+      });
+
+      it("should not render when the value of loop.seenToS is set to 'seen'",
+        function() {
+        var ToSView = new loop.panel.ToSView({el: $('#tos-view')});
+        fakeSeenToSPref = 1;
+
+        ToSView.render();
+
+        sinon.assert.notCalled(navigator.mozLoop.setLoopCharPref);
+      });
     });
   });
 });
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -14,39 +14,34 @@
   </div>
   <div id="messages"></div>
   <div id="fixtures"></div>
 
   <!-- libs -->
   <script src="../../content/shared/libs/jquery-2.1.0.js"></script>
   <script src="../../content/shared/libs/lodash-2.4.1.js"></script>
   <script src="../../content/shared/libs/backbone-1.1.2.js"></script>
-  <script src="../../content/shared/libs/sjcl-dev20140604.js"></script>
-  <script src="../../content/shared/libs/token.js"></script>
-  <script src="../../content/shared/libs/hawk-browser-2.2.1.js"></script>
-  <script src="../../standalone/content/libs/webl10n-20130617.js"></script>
+ <script src="../../standalone/content/libs/webl10n-20130617.js"></script>
 
   <!-- test dependencies -->
   <script src="vendor/mocha-1.17.1.js"></script>
   <script src="vendor/chai-1.9.0.js"></script>
   <script src="vendor/sinon-1.9.0.js"></script>
   <script>
     /*global chai, mocha */
     chai.Assertion.includeStack = true;
     mocha.setup('bdd');
   </script>
 
   <!-- App scripts -->
-  <script src="../../content/shared/js/client.js"></script>
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/router.js"></script>
 
   <!-- Test scripts -->
-  <script src="client_test.js"></script>
   <script src="models_test.js"></script>
   <script src="views_test.js"></script>
   <script src="router_test.js"></script>
   <script>
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
   </script>
--- a/browser/components/loop/test/shared/models_test.js
+++ b/browser/components/loop/test/shared/models_test.js
@@ -47,94 +47,100 @@ describe("loop.shared.models", function(
       it("should require a sdk option", function() {
         expect(function() {
           new sharedModels.ConversationModel();
         }).to.Throw(Error, /missing required sdk/);
       });
     });
 
     describe("constructed", function() {
-      var conversation, reqCallInfoStub, reqCallsInfoStub, fakeBaseServerUrl;
+      var conversation, fakeClient, fakeBaseServerUrl,
+          requestCallInfoStub, requestCallsInfoStub;
 
       beforeEach(function() {
         conversation = new sharedModels.ConversationModel({}, {sdk: fakeSDK});
         conversation.set("loopToken", "fakeToken");
         fakeBaseServerUrl = "http://fakeBaseServerUrl";
-        reqCallInfoStub = sandbox.stub(loop.shared.Client.prototype,
-          "requestCallInfo");
-        reqCallsInfoStub = sandbox.stub(loop.shared.Client.prototype,
-          "requestCallsInfo");
+        fakeClient = {
+          requestCallInfo: sandbox.stub(),
+          requestCallsInfo: sandbox.stub()
+        };
+        requestCallInfoStub = fakeClient.requestCallInfo;
+        requestCallsInfoStub = fakeClient.requestCallsInfo;
       });
 
       describe("#initiate", function() {
         it("call requestCallInfo on the client for outgoing calls",
           function() {
             conversation.initiate({
-              baseServerUrl: fakeBaseServerUrl,
-              outgoing: true
+              client: fakeClient,
+              outgoing: true,
+              callType: "audio"
             });
 
-            sinon.assert.calledOnce(reqCallInfoStub);
-            sinon.assert.calledWith(reqCallInfoStub, "fakeToken");
+            sinon.assert.calledOnce(requestCallInfoStub);
+            sinon.assert.calledWith(requestCallInfoStub, "fakeToken", "audio");
           });
 
         it("should not call requestCallsInfo on the client for outgoing calls",
           function() {
             conversation.initiate({
-              baseServerUrl: fakeBaseServerUrl,
-              outgoing: true
+              client: fakeClient,
+              outgoing: true,
+              callType: "audio"
             });
 
-            sinon.assert.notCalled(reqCallsInfoStub);
+            sinon.assert.notCalled(requestCallsInfoStub);
           });
 
         it("call requestCallsInfo on the client for incoming calls",
           function() {
+            conversation.set("loopVersion", 42);
             conversation.initiate({
-              baseServerUrl: fakeBaseServerUrl,
+              client: fakeClient,
               outgoing: false
             });
 
-            sinon.assert.calledOnce(reqCallsInfoStub);
-            sinon.assert.calledWith(reqCallsInfoStub);
+            sinon.assert.calledOnce(requestCallsInfoStub);
+            sinon.assert.calledWith(requestCallsInfoStub, 42);
           });
 
         it("should not call requestCallInfo on the client for incoming calls",
           function() {
             conversation.initiate({
-              baseServerUrl: fakeBaseServerUrl,
+              client: fakeClient,
               outgoing: false
             });
 
-            sinon.assert.notCalled(reqCallInfoStub);
+            sinon.assert.notCalled(requestCallInfoStub);
           });
 
         it("should update conversation session information from server data",
           function() {
             sandbox.stub(conversation, "setReady");
-            reqCallInfoStub.callsArgWith(1, null, fakeSessionData);
+            requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
 
             conversation.initiate({
-              baseServerUrl: fakeBaseServerUrl,
+              client: fakeClient,
               outgoing: true
             });
 
             sinon.assert.calledOnce(conversation.setReady);
             sinon.assert.calledWith(conversation.setReady, fakeSessionData);
           });
 
         it("should trigger a `session:error` on failure", function(done) {
-          reqCallInfoStub.callsArgWith(1,
+          requestCallInfoStub.callsArgWith(2,
             new Error("failed: HTTP 400 Bad Request; fake"));
 
           conversation.on("session:error", function(err) {
             expect(err.message).to.match(/failed: HTTP 400 Bad Request; fake/);
             done();
           }).initiate({
-            baseServerUrl: fakeBaseServerUrl,
+            client: fakeClient,
             outgoing: true
           });
         });
       });
 
       describe("#setReady", function() {
         it("should update conversation session information", function() {
           conversation.setReady(fakeSessionData);
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -28,18 +28,20 @@
   <script>
     chai.Assertion.includeStack = true;
     mocha.setup('bdd');
   </script>
   <!-- App scripts -->
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/router.js"></script>
+  <script src="../../standalone/content/js/standaloneClient.js"></script>
   <script src="../../standalone/content/js/webapp.js"></script>
-  <!-- Test scripts -->
+ <!-- Test scripts -->
+  <script src="standalone_client_test.js"></script>
   <script src="webapp_test.js"></script>
   <script>
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
 </script>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/standalone/standalone_client_test.js
@@ -0,0 +1,112 @@
+/* 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/. */
+
+/*global loop, sinon, it, beforeEach, afterEach, describe, hawk */
+
+var expect = chai.expect;
+
+describe("loop.StandaloneClient", function() {
+  "use strict";
+
+  var sandbox,
+      fakeXHR,
+      requests = [],
+      callback,
+      fakeToken;
+
+  var fakeErrorRes = JSON.stringify({
+      status: "errors",
+      errors: [{
+        location: "url",
+        name: "token",
+        description: "invalid token"
+      }]
+    });
+
+  beforeEach(function() {
+    sandbox = sinon.sandbox.create();
+    fakeXHR = sandbox.useFakeXMLHttpRequest();
+    requests = [];
+    // https://github.com/cjohansen/Sinon.JS/issues/393
+    fakeXHR.xhr.onCreate = function (xhr) {
+      requests.push(xhr);
+    };
+    callback = sinon.spy();
+    fakeToken = "fakeTokenText";
+  });
+
+  afterEach(function() {
+    sandbox.restore();
+  });
+
+  describe("loop.StandaloneClient", function() {
+    describe("#constructor", function() {
+      it("should require a baseServerUrl setting", function() {
+        expect(function() {
+          new loop.StandaloneClient();
+        }).to.Throw(Error, /required/);
+      });
+    });
+
+    describe("requestCallInfo", function() {
+      var client;
+
+      beforeEach(function() {
+        client = new loop.StandaloneClient(
+          {baseServerUrl: "http://fake.api"}
+        );
+      });
+
+      it("should prevent launching a conversation when token is missing",
+        function() {
+          expect(function() {
+            client.requestCallInfo();
+          }).to.Throw(Error, /missing.*[Tt]oken/);
+        });
+
+      it("should post data for the given call", function() {
+        client.requestCallInfo("fake", "audio", callback);
+
+        expect(requests).to.have.length.of(1);
+        expect(requests[0].url).to.be.equal("http://fake.api/calls/fake");
+        expect(requests[0].method).to.be.equal("POST");
+        expect(requests[0].requestBody).to.be.equal('{"callType":"audio"}');
+      });
+
+      it("should receive call data for the given call", function() {
+        client.requestCallInfo("fake", "audio-video", callback);
+
+        var sessionData = {
+          sessionId: "one",
+          sessionToken: "two",
+          apiKey: "three"
+        };
+
+        requests[0].respond(200, {"Content-Type": "application/json"},
+                            JSON.stringify(sessionData));
+        sinon.assert.calledWithExactly(callback, null, sessionData);
+      });
+
+      it("should send an error when the request fails", function() {
+        client.requestCallInfo("fake", "audio", callback);
+
+        requests[0].respond(400, {"Content-Type": "application/json"},
+                            fakeErrorRes);
+        sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
+          return /400.*invalid token/.test(err.message);
+        }));
+      });
+
+      it("should send an error if the data is not valid", function() {
+        client.requestCallInfo("fake", "audio", callback);
+
+        requests[0].respond(200, {"Content-Type": "application/json"},
+                            '{"bad": "one"}');
+        sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
+          return /Invalid data received/.test(err.message);
+        }));
+      });
+    });
+  });
+});
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -276,20 +276,21 @@ describe("loop.webapp", function() {
 
       it("should start the conversation establishment process", function() {
         conversation.set("loopToken", "fake");
 
         view.initiate(fakeSubmitEvent);
 
         sinon.assert.calledOnce(fakeSubmitEvent.preventDefault);
         sinon.assert.calledOnce(initiate);
-        sinon.assert.calledWith(initiate, {
-          baseServerUrl: loop.webapp.baseServerUrl,
-          outgoing: true
-        });
+        sinon.assert.calledWith(initiate, sinon.match(function (value) {
+          return !!value.outgoing &&
+            (value.client instanceof loop.StandaloneClient) &&
+            value.client.settings.baseServerUrl === loop.webapp.baseServerUrl;
+        }, "{client: <properly constructed client>, outgoing: true}"));
       });
 
       it("should disable current form once session is initiated", function() {
         sandbox.stub(view, "disableForm");
         conversation.set("loopToken", "fake");
 
         view.initiate(fakeSubmitEvent);
 
--- a/browser/components/preferences/in-content/advanced.xul
+++ b/browser/components/preferences/in-content/advanced.xul
@@ -354,17 +354,17 @@
       </groupbox>
     </tabpanel>
 
     <!-- Update -->
     <tabpanel id="updatePanel" orient="vertical">
 #ifdef MOZ_UPDATER
       <groupbox id="updateApp" align="start">
         <caption><label>&updateApp.label;</label></caption>
-        <radiogroup id="updateRadioGroup"
+        <radiogroup id="updateRadioGroup" align="start"
                     oncommand="gAdvancedPane.updateWritePrefs();">
 #ifdef XP_WIN
 #ifdef MOZ_METRO
               <radio id="autoMetro"
                      value="autoMetro"
                      label="&updateAutoMetro.label;"
                      accesskey="&updateAutoMetro.accesskey;"/>
               <hbox id="autoMetroIndent"
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -146,38 +146,47 @@
           >&dontrememberActions.clearHistory.label;</html:a>&dontrememberActions.post.label;</description>
         </vbox>
         <spacer flex="1" class="indent"/>
       </hbox>
     </vbox>
     <vbox id="historyCustomPane">
       <separator class="thin"/>
       <vbox class="indent">
-        <checkbox id="privateBrowsingAutoStart"
-                  label="&privateBrowsingPermanent2.label;"
-                  accesskey="&privateBrowsingPermanent2.accesskey;"
-                  preference="browser.privatebrowsing.autostart"
-                  oncommand="gPrivacyPane.updateAutostart()"/>
-
+        <hbox>
+          <checkbox id="privateBrowsingAutoStart"
+                    label="&privateBrowsingPermanent2.label;"
+                    accesskey="&privateBrowsingPermanent2.accesskey;"
+                    preference="browser.privatebrowsing.autostart"
+                    oncommand="gPrivacyPane.updateAutostart()"/>
+          <spacer flex="1"/>
+        </hbox>
         <vbox class="indent">
-          <checkbox id="rememberHistory"
-                    label="&rememberHistory2.label;"
-                    accesskey="&rememberHistory2.accesskey;"
-                    preference="places.history.enabled"/>
-          <checkbox id="rememberForms"
-                    label="&rememberSearchForm.label;"
-                    accesskey="&rememberSearchForm.accesskey;"
-                    preference="browser.formfill.enable"/>
+          <hbox>
+            <checkbox id="rememberHistory"
+                      label="&rememberHistory2.label;"
+                      accesskey="&rememberHistory2.accesskey;"
+                      preference="places.history.enabled"/>
+            <spacer flex="1"/>
+          </hbox>
+          <hbox>
+            <checkbox id="rememberForms"
+                      label="&rememberSearchForm.label;"
+                      accesskey="&rememberSearchForm.accesskey;"
+                      preference="browser.formfill.enable"/>
+            <spacer flex="1"/>
+          </hbox>
 
           <hbox id="cookiesBox">
-            <checkbox id="acceptCookies" label="&acceptCookies.label;" flex="1"
+            <checkbox id="acceptCookies" label="&acceptCookies.label;"
                       preference="network.cookie.cookieBehavior"
                       accesskey="&acceptCookies.accesskey;"
                       onsyncfrompreference="return gPrivacyPane.readAcceptCookies();"
                       onsynctopreference="return gPrivacyPane.writeAcceptCookies();"/>
+            <spacer flex="1"/>
             <button id="cookieExceptions" oncommand="gPrivacyPane.showCookieExceptions();"
                     label="&cookieExceptions.label;" accesskey="&cookieExceptions.accesskey;"
                     preference="pref.privacy.disable_button.cookie_exceptions"/>
           </hbox>
           <hbox id="acceptThirdPartyRow"
                 class="indent"
                 align="center">
             <label id="acceptThirdPartyLabel" control="acceptThirdPartyMenu"
@@ -209,20 +218,21 @@
             <spacer flex="1"/>
             <button id="showCookiesButton"
                     label="&showCookies.label;" accesskey="&showCookies.accesskey;"
                     oncommand="gPrivacyPane.showCookies();"
                     preference="pref.privacy.disable_button.view_cookies"/>
           </hbox>
           <hbox id="clearDataBox"
                 align="center">
-            <checkbox id="alwaysClear" flex="1"
+            <checkbox id="alwaysClear"
                       preference="privacy.sanitize.sanitizeOnShutdown"
                       label="&clearOnClose.label;"
                       accesskey="&clearOnClose.accesskey;"/>
+            <spacer flex="1"/>
             <button id="clearDataSettings" label="&clearOnCloseSettings.label;"
                     accesskey="&clearOnCloseSettings.accesskey;"
                     oncommand="gPrivacyPane.showClearPrivateDataSettings();"/>
           </hbox>
         </vbox>
       </vbox>
     </vbox>
   </deck>
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -271,17 +271,17 @@
           </vbox>
         </hbox>
       </deck>
     </groupbox>
 
     <groupbox id="syncOptions">
       <caption><label>&syncBrand.shortName.label;</label></caption>
       <hbox id="fxaSyncEngines">
-        <vbox>
+        <vbox align="start">
           <checkbox label="&engine.tabs.label;"
                     accesskey="&engine.tabs.accesskey;"
                     preference="engine.tabs"/>
           <checkbox label="&engine.bookmarks.label;"
                     accesskey="&engine.bookmarks.accesskey;"
                     preference="engine.bookmarks"/>
           <hbox>
             <checkbox id="fxa-pweng-chk"
--- a/browser/devtools/canvasdebugger/test/browser_canvas-actor-test-02.js
+++ b/browser/devtools/canvasdebugger/test/browser_canvas-actor-test-02.js
@@ -9,17 +9,18 @@
 function ifTestingSupported() {
   let [target, debuggee, front] = yield initCallWatcherBackend(SIMPLE_CANVAS_URL);
 
   let navigated = once(target, "navigate");
 
   yield front.setup({
     tracedGlobals: ["CanvasRenderingContext2D", "WebGLRenderingContext"],
     startRecording: true,
-    performReload: true
+    performReload: true,
+    storeCalls: true
   });
   ok(true, "The front was setup up successfully.");
 
   yield navigated;
   ok(true, "Target automatically navigated when the front was set up.");
 
   // Allow the content to execute some functions.
   yield waitForTick();
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -119,11 +119,13 @@ browser.jar:
     content/browser/devtools/app-manager/projects.xhtml                (app-manager/content/projects.xhtml)
     content/browser/devtools/app-manager/index.xul                     (app-manager/content/index.xul)
     content/browser/devtools/app-manager/index.js                      (app-manager/content/index.js)
     content/browser/devtools/app-manager/help.xhtml                    (app-manager/content/help.xhtml)
     content/browser/devtools/app-manager/manifest-editor.js            (app-manager/content/manifest-editor.js)
     content/browser/devtools/graphs-frame.xhtml                        (shared/widgets/graphs-frame.xhtml)
     content/browser/devtools/spectrum-frame.xhtml                      (shared/widgets/spectrum-frame.xhtml)
     content/browser/devtools/spectrum.css                              (shared/widgets/spectrum.css)
+    content/browser/devtools/cubic-bezier-frame.xhtml                  (shared/widgets/cubic-bezier-frame.xhtml)
+    content/browser/devtools/cubic-bezier.css                          (shared/widgets/cubic-bezier.css)
     content/browser/devtools/eyedropper.xul                            (eyedropper/eyedropper.xul)
     content/browser/devtools/eyedropper/crosshairs.css                 (eyedropper/crosshairs.css)
     content/browser/devtools/eyedropper/nocursor.css                   (eyedropper/nocursor.css)
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -1940,16 +1940,29 @@ CustomRequestView.prototype = {
 function NetworkDetailsView() {
   dumpn("NetworkDetailsView was instantiated");
 
   this._onTabSelect = this._onTabSelect.bind(this);
 };
 
 NetworkDetailsView.prototype = {
   /**
+   * An object containing the state of tabs.
+   */
+  _viewState: {
+    // if updating[tab] is true a task is currently updating the given tab.
+    updating: [],
+    // if dirty[tab] is true, the tab needs to be repopulated once current
+    // update task finishes
+    dirty: [],
+    // the most recently received attachment data for the request
+    latestData: null,
+  },
+
+  /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function() {
     dumpn("Initializing the NetworkDetailsView");
 
     this.widget = $("#event-details-pane");
 
     this._headers = new VariablesView($("#all-headers"),
@@ -2044,17 +2057,29 @@ NetworkDetailsView.prototype = {
     let tab = this.widget.selectedIndex;
     let view = this;
 
     // Make sure the data source is valid and don't populate the same tab twice.
     if (!src || populated[tab]) {
       return;
     }
 
+    let viewState = this._viewState;
+    if (viewState.updating[tab]) {
+      // A task is currently updating this tab. If we started another update
+      // task now it would result in a duplicated content as described in bugs
+      // 997065 and 984687. As there's no way to stop the current task mark the
+      // tab dirty and refresh the panel once the current task finishes.
+      viewState.dirty[tab] = true;
+      viewState.latestData = src;
+      return;
+    }
+
     Task.spawn(function*() {
+      viewState.updating[tab] = true;
       switch (tab) {
         case 0: // "Headers"
           yield view._setSummary(src);
           yield view._setResponseHeaders(src.responseHeaders);
           yield view._setRequestHeaders(
             src.requestHeaders,
             src.requestHeadersFromUploadStream);
           break;
@@ -2074,23 +2099,42 @@ NetworkDetailsView.prototype = {
           break;
         case 4: // "Timings"
           yield view._setTimingsInformation(src.eventTimings);
           break;
         case 5: // "Preview"
           yield view._setHtmlPreview(src.responseContent);
           break;
       }
-      populated[tab] = true;
-      window.emit(EVENTS.TAB_UPDATED);
-
-      if (NetMonitorController.isConnected()) {
-        NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
+      viewState.updating[tab] = false;
+    }).then(() => {
+      if (tab == this.widget.selectedIndex) {
+        if (viewState.dirty[tab]) {
+          // The request information was updated while the task was running.
+          viewState.dirty[tab] = false;
+          view.populate(viewState.latestData);
+        }
+        else {
+          // Tab is selected but not dirty. We're done here.
+          populated[tab] = true;
+          window.emit(EVENTS.TAB_UPDATED);
+
+          if (NetMonitorController.isConnected()) {
+            NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
+          }
+        }
       }
-    });
+      else {
+        if (viewState.dirty[tab]) {
+          // Tab is dirty but no longer selected. Don't refresh it now, it'll be
+          // done if the tab is shown again.
+          viewState.dirty[tab] = false;
+        }
+      }
+    }, Cu.reportError);
   },
 
   /**
    * Sets the network request summary shown in this view.
    *
    * @param object aData
    *        The data source (this should be the attachment of a request item).
    */
--- a/browser/devtools/netmonitor/test/browser.ini
+++ b/browser/devtools/netmonitor/test/browser.ini
@@ -47,16 +47,17 @@ support-files =
 [browser_net_complex-params.js]
 [browser_net_content-type.js]
 [browser_net_curl-utils.js]
 [browser_net_copy_image_as_data_uri.js]
 [browser_net_copy_url.js]
 [browser_net_copy_as_curl.js]
 [browser_net_cyrillic-01.js]
 [browser_net_cyrillic-02.js]
+[browser_net_details-no-duplicated-content.js]
 [browser_net_filter-01.js]
 [browser_net_filter-02.js]
 [browser_net_filter-03.js]
 [browser_net_filter-04.js]
 [browser_net_footer-summary.js]
 [browser_net_html-preview.js]
 [browser_net_icon-preview.js]
 [browser_net_image-tooltip.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_details-no-duplicated-content.js
@@ -0,0 +1,112 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// A test to ensure that the content in details pane is not duplicated.
+
+let test = Task.async(function* () {
+  info("Initializing test");
+  let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
+  let panel = monitor.panelWin;
+  let { NetMonitorView, EVENTS } = panel;
+  let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+  let TEST_CASES = [
+    {
+      desc: "Test headers tab",
+      pageURI: CUSTOM_GET_URL,
+      isPost: false,
+      tabIndex: 0,
+      variablesView: NetworkDetails._headers,
+      expectedScopeLength: 2,
+    },
+    {
+      desc: "Test cookies tab",
+      pageURI: CUSTOM_GET_URL,
+      isPost: false,
+      tabIndex: 1,
+      variablesView: NetworkDetails._cookies,
+      expectedScopeLength: 1,
+    },
+    {
+      desc: "Test params tab",
+      pageURI: POST_RAW_URL,
+      isPost: true,
+      tabIndex: 2,
+      variablesView: NetworkDetails._params,
+      expectedScopeLength: 1,
+    },
+  ];
+
+  info("Adding a cookie for the \"Cookie\" tab test");
+  debuggee.document.cookie = "a=b; Max-Age=10; path=" +  CUSTOM_GET_URL;
+  is(debuggee.document.cookie, "a=b", "Cookie was added.")
+
+  info("Running tests");
+  for (let spec of TEST_CASES) {
+    yield runTestCase(spec);
+  }
+
+  // Remove the cookie. If an error occurs Max-Age ensures it doesn't stay to
+  // mess with the tests.
+  info("Removing the added cookie.");
+  debuggee.document.cookie = "a=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+  is(debuggee.document.cookie, "", "Cookie was removed.");
+
+  yield teardown(monitor);
+  finish();
+
+  /**
+   * A helper that handles the execution of each case.
+   */
+  function* runTestCase(spec) {
+    info("Running case: " + spec.desc);
+    debuggee.content.location = spec.pageURI;
+
+    yield waitForNetworkEvents(monitor, 1);
+    RequestsMenu.clear();
+    yield waitForFinalDetailTabUpdate(spec.tabIndex, spec.isPost);
+
+    is(spec.variablesView._store.length, spec.expectedScopeLength,
+       "View contains " + spec.expectedScopeLength + " scope headers");
+  }
+
+  /**
+   * A helper that prepares the variables view for the actual testing. It
+   * - selects the correct tab
+   * - performs the specified request
+   * - opens the details view
+   * - waits for the final update to happen
+   */
+  function* waitForFinalDetailTabUpdate(tabIndex, isPost) {
+    let onNetworkEvent = waitFor(panel, EVENTS.NETWORK_EVENT);
+    let onDetailsPopulated = waitFor(panel, EVENTS.NETWORKDETAILSVIEW_POPULATED);
+    let onRequestFinished = isPost ?
+      waitForNetworkEvents(monitor, 0, 1) : waitForNetworkEvents(monitor, 1);
+
+    info("Performing a request");
+    debuggee.performRequests(1, null);
+
+    info("Waiting for NETWORK_EVENT");
+    yield onNetworkEvent;
+
+    ok(true, "Received NETWORK_EVENT. Selecting the item.");
+    let item = RequestsMenu.getItemAtIndex(0);
+    RequestsMenu.selectedItem = item;
+
+    info("Item selected. Waiting for NETWORKDETAILSVIEW_POPULATED");
+    yield onDetailsPopulated;
+
+    info("Selecting tab at index " + tabIndex);
+    NetworkDetails.widget.selectedIndex = tabIndex;
+
+    ok(true, "Received NETWORKDETAILSVIEW_POPULATED. Waiting for request to finish");
+    yield onRequestFinished;
+
+    ok(true, "Request finished. Waiting for tab update to complete");
+    let onDetailsUpdateFinished = waitFor(panel, EVENTS.TAB_UPDATED);
+    yield onDetailsUpdateFinished;
+    ok(true, "Details were updated");
+  }
+});
--- a/browser/devtools/netmonitor/test/browser_net_status-codes.js
+++ b/browser/devtools/netmonitor/test/browser_net_status-codes.js
@@ -1,155 +1,203 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
 
 /**
  * Tests if requests display the correct status code and text in the UI.
  */
 
-function test() {
-  initNetMonitor(STATUS_CODES_URL).then(([aTab, aDebuggee, aMonitor]) => {
-    info("Starting test... ");
+let test = Task.async(function*() {
+  let [tab, debuggee, monitor] = yield initNetMonitor(STATUS_CODES_URL);
+
+  info("Starting test... ");
 
-    let { document, L10N, NetMonitorView } = aMonitor.panelWin;
-    let { RequestsMenu, NetworkDetails } = NetMonitorView;
+  let { document, L10N, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu, NetworkDetails } = NetMonitorView;
+  let requestItems = [];
 
-    RequestsMenu.lazyUpdate = false;
-    NetworkDetails._params.lazyEmpty = false;
-
-    waitForNetworkEvents(aMonitor, 5).then(() => {
-      let requestItems = [];
+  RequestsMenu.lazyUpdate = false;
+  NetworkDetails._params.lazyEmpty = false;
 
-      verifyRequestItemTarget(requestItems[0] = RequestsMenu.getItemAtIndex(0),
-        "GET", STATUS_CODES_SJS + "?sts=100", {
-          status: 101,
-          statusText: "Switching Protocols",
-          type: "plain",
-          fullMimeType: "text/plain; charset=utf-8",
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
-          time: true
-        });
-      verifyRequestItemTarget(requestItems[1] = RequestsMenu.getItemAtIndex(1),
-        "GET", STATUS_CODES_SJS + "?sts=200", {
-          status: 202,
-          statusText: "Created",
-          type: "plain",
-          fullMimeType: "text/plain; charset=utf-8",
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
-          time: true
-        });
-      verifyRequestItemTarget(requestItems[2] = RequestsMenu.getItemAtIndex(2),
-        "GET", STATUS_CODES_SJS + "?sts=300", {
-          status: 303,
-          statusText: "See Other",
-          type: "plain",
-          fullMimeType: "text/plain; charset=utf-8",
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
-          time: true
-        });
-      verifyRequestItemTarget(requestItems[3] = RequestsMenu.getItemAtIndex(3),
-        "GET", STATUS_CODES_SJS + "?sts=400", {
-          status: 404,
-          statusText: "Not Found",
-          type: "plain",
-          fullMimeType: "text/plain; charset=utf-8",
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
-          time: true
-        });
-      verifyRequestItemTarget(requestItems[4] = RequestsMenu.getItemAtIndex(4),
-        "GET", STATUS_CODES_SJS + "?sts=500", {
-          status: 501,
-          statusText: "Not Implemented",
-          type: "plain",
-          fullMimeType: "text/plain; charset=utf-8",
-          size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
-          time: true
-        });
+  const REQUEST_DATA = [
+    { // request #0
+      method: "GET",
+      uri: STATUS_CODES_SJS + "?sts=100",
+      details: {
+        status: 101,
+        statusText: "Switching Protocols",
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
+        time: true
+      }
+    },
+    { // request #1
+      method: "GET",
+      uri: STATUS_CODES_SJS + "?sts=200",
+      details: {
+        status: 202,
+        statusText: "Created",
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
+        time: true
+      }
+    },
+    { // request #2
+      method: "GET",
+      uri: STATUS_CODES_SJS + "?sts=300",
+      details: {
+        status: 303,
+        statusText: "See Other",
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0),
+        time: true
+      }
+    },
+    { // request #3
+      method: "GET",
+      uri: STATUS_CODES_SJS + "?sts=400",
+      details: {
+        status: 404,
+        statusText: "Not Found",
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
+        time: true
+      }
+    },
+    { // request #4
+      method: "GET",
+      uri: STATUS_CODES_SJS + "?sts=500",
+      details: {
+        status: 501,
+        statusText: "Not Implemented",
+        type: "plain",
+        fullMimeType: "text/plain; charset=utf-8",
+        size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
+        time: true
+      }
+    }
+  ];
 
-      // Test summaries...
-      EventUtils.sendMouseEvent({ type: "mousedown" },
-        document.querySelectorAll("#details-pane tab")[0]);
+  debuggee.performRequests();
+  yield waitForNetworkEvents(monitor, 5);
+
+  info("Performing tests");
+  yield verifyRequests();
+  yield testTab(0, testSummary);
+  yield testTab(2, testParams);
+
+  yield teardown(monitor);
+  finish();
 
-      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[0].target);
-      testSummary("GET", STATUS_CODES_SJS + "?sts=100", "101", "Switching Protocols");
-
-      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[1].target);
-      testSummary("GET", STATUS_CODES_SJS + "?sts=200", "202", "Created");
-
-      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[2].target);
-      testSummary("GET", STATUS_CODES_SJS + "?sts=300", "303", "See Other");
+  /**
+   * A helper that verifies all requests show the correct information and caches
+   * RequestsMenu items to requestItems array.
+   */
+  function* verifyRequests() {
+    info("Verifying requests contain correct information.");
+    let index = 0;
+    for (let request of REQUEST_DATA) {
+      let item = RequestsMenu.getItemAtIndex(index);
+      requestItems[index] = item;
 
-      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[3].target);
-      testSummary("GET", STATUS_CODES_SJS + "?sts=400", "404", "Not Found");
-
-      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[4].target);
-      testSummary("GET", STATUS_CODES_SJS + "?sts=500", "501", "Not Implemented");
+      info("Verifying request #" + index);
+      yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
 
-      // Test params...
-      EventUtils.sendMouseEvent({ type: "mousedown" },
-        document.querySelectorAll("#details-pane tab")[2]);
-
-      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[0].target);
-      testParamsTab("\"100\"");
-
-      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[1].target);
-      testParamsTab("\"200\"");
+      index++;
+    }
+  }
 
-      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[2].target);
-      testParamsTab("\"300\"");
-
-      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[3].target);
-      testParamsTab("\"400\"");
+  /**
+   * A helper that opens a given tab of request details pane, selects and passes
+   * all requests to the given test function.
+   *
+   * @param Number tab
+   *               The index of NetworkDetails tab to activate.
+   * @param Function testFn(requestItem)
+   *        A function that should perform all necessary tests. It's called once
+   *        for every item of REQUEST_DATA with that item being selected in the
+   *        NetworkMonitor.
+   */
+  function* testTab(tab, testFn) {
+    info("Testing tab #" + tab);
+    EventUtils.sendMouseEvent({ type: "mousedown" },
+          document.querySelectorAll("#details-pane tab")[tab]);
 
-      EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[4].target);
-      testParamsTab("\"500\"");
+    let counter = 0;
+    for (let item of REQUEST_DATA) {
+      info("Waiting tab #" + tab + " to update with request #" + counter);
+      yield chooseRequest(counter);
+
+      info("Tab updated. Performing checks");
+      yield testFn(item);
 
-      // We're done here.
-      teardown(aMonitor).then(finish);
+      counter++;
+    }
+  }
 
-      function testSummary(aMethod, aUrl, aStatus, aStatusText) {
-        let tab = document.querySelectorAll("#details-pane tab")[0];
-        let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
+  /**
+   * A function that tests "Summary" contains correct information.
+   */
+  function* testSummary(data) {
+    let tab = document.querySelectorAll("#details-pane tab")[0];
+    let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
 
-        is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
-          aUrl, "The url summary value is incorrect.");
-        is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
-          aMethod, "The method summary value is incorrect.");
-        is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
-          aStatus, "The status summary code is incorrect.");
-        is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
-          aStatus + " " + aStatusText, "The status summary value is incorrect.");
-      }
+    let { method, uri, details: { status, statusText } } = data;
+    is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
+      uri, "The url summary value is incorrect.");
+    is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
+      method, "The method summary value is incorrect.");
+    is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
+      status, "The status summary code is incorrect.");
+    is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
+      status + " " + statusText, "The status summary value is incorrect.");
+  }
 
-      function testParamsTab(aStatusParamValue) {
-        let tab = document.querySelectorAll("#details-pane tab")[2];
-        let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+  /**
+   * A function that tests "Params" tab contains correct information.
+   */
+  function* testParams(data) {
+    let tab = document.querySelectorAll("#details-pane tab")[2];
+    let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+    let statusParamValue = data.uri.split("=").pop();
+    let statusParamShownValue = "\"" + statusParamValue + "\"";
 
-        is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
-          "There should be 1 param scope displayed in this tabpanel.");
-        is(tabpanel.querySelectorAll(".variable-or-property").length, 1,
-          "There should be 1 param value displayed in this tabpanel.");
-        is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
-          "The empty notice should not be displayed in this tabpanel.");
+    is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+      "There should be 1 param scope displayed in this tabpanel.");
+    is(tabpanel.querySelectorAll(".variable-or-property").length, 1,
+      "There should be 1 param value displayed in this tabpanel.");
+    is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+      "The empty notice should not be displayed in this tabpanel.");
 
-        let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+    let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
 
-        is(paramsScope.querySelector(".name").getAttribute("value"),
-          L10N.getStr("paramsQueryString"),
-          "The params scope doesn't have the correct title.");
+    is(paramsScope.querySelector(".name").getAttribute("value"),
+      L10N.getStr("paramsQueryString"),
+      "The params scope doesn't have the correct title.");
 
-        is(paramsScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
-          "sts", "The param name was incorrect.");
-        is(paramsScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
-          aStatusParamValue, "The param value was incorrect.");
+    is(paramsScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+      "sts", "The param name was incorrect.");
+    is(paramsScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+      statusParamShownValue, "The param value was incorrect.");
 
-        is(tabpanel.querySelector("#request-params-box")
-          .hasAttribute("hidden"), false,
-          "The request params box should not be hidden.");
-        is(tabpanel.querySelector("#request-post-data-textarea-box")
-          .hasAttribute("hidden"), true,
-          "The request post data textarea box should be hidden.");
-      }
-    });
+    is(tabpanel.querySelector("#request-params-box")
+      .hasAttribute("hidden"), false,
+      "The request params box should not be hidden.");
+    is(tabpanel.querySelector("#request-post-data-textarea-box")
+      .hasAttribute("hidden"), true,
+      "The request post data textarea box should be hidden.");
+  }
 
-    aDebuggee.performRequests();
-  });
-}
+  /**
+   * A helper that clicks on a specified request and returns a promise resolved
+   * when NetworkDetails has been populated with the data of the given request.
+   */
+  function chooseRequest(index) {
+    EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[index].target);
+    return waitFor(monitor.panelWin, monitor.panelWin.EVENTS.TAB_UPDATED);
+  }
+});
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -7,16 +7,19 @@ support-files =
   browser_layoutHelpers_iframe.html
   browser_templater_basic.html
   browser_toolbar_basic.html
   browser_toolbar_webconsole_errors_count.html
   head.js
   leakhunt.js
 
 [browser_css_color.js]
+[browser_cubic-bezier-01.js]
+[browser_cubic-bezier-02.js]
+[browser_cubic-bezier-03.js]
 [browser_graphs-01.js]
 [browser_graphs-02.js]
 [browser_graphs-03.js]
 [browser_graphs-04.js]
 [browser_graphs-05.js]
 [browser_graphs-06.js]
 [browser_graphs-07.js]
 [browser_graphs-08.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_cubic-bezier-01.js
@@ -0,0 +1,36 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the CubicBezierWidget generates content in a given parent node
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget");
+
+let test = Task.async(function*() {
+  yield promiseTab(TEST_URI);
+
+  info("Checking that the markup is created in the parent");
+  let container = content.document.querySelector("#container");
+  let w = new CubicBezierWidget(container);
+
+  ok(container.querySelector(".coordinate-plane"),
+    "The coordinate plane has been added");
+  let buttons = container.querySelectorAll("button");
+  is(buttons.length, 2,
+    "The 2 control points have been added");
+  is(buttons[0].className, "control-point");
+  is(buttons[0].id, "P1");
+  is(buttons[1].className, "control-point");
+  is(buttons[1].id, "P2");
+  ok(container.querySelector("canvas"), "The curve canvas has been added");
+
+  info("Destroying the widget");
+  w.destroy();
+  is(container.children.length, 0, "All nodes have been removed");
+
+  gBrowser.removeCurrentTab();
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_cubic-bezier-02.js
@@ -0,0 +1,152 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the CubicBezierWidget events
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierWidget, PREDEFINED} =
+  devtools.require("devtools/shared/widgets/CubicBezierWidget");
+
+let test = Task.async(function*() {
+  yield promiseTab(TEST_URI);
+
+  let container = content.document.querySelector("#container");
+  let w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+  yield pointsCanBeDragged(w);
+  yield curveCanBeClicked(w);
+  yield pointsCanBeMovedWithKeyboard(w);
+
+  w.destroy();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* pointsCanBeDragged(widget) {
+  info("Checking that the control points can be dragged with the mouse");
+
+  info("Listening for the update event");
+  let onUpdated = widget.once("updated");
+
+  info("Generating a mousedown/move/up on P1");
+  widget._onPointMouseDown({target: widget.p1});
+  EventUtils.synthesizeMouse(content.document.documentElement, 0, 100,
+    {type: "mousemove"}, content.window);
+  EventUtils.synthesizeMouse(content.document.documentElement, 0, 100,
+    {type: "mouseup"}, content.window);
+
+  let bezier = yield onUpdated;
+  ok(true, "The widget fired the updated event");
+  ok(bezier, "The updated event contains a bezier argument");
+  is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
+
+  info("Listening for the update event");
+  let onUpdated = widget.once("updated");
+
+  info("Generating a mousedown/move/up on P2");
+  widget._onPointMouseDown({target: widget.p2});
+  EventUtils.synthesizeMouse(content.document.documentElement, 200, 300,
+    {type: "mousemove"}, content.window);
+  EventUtils.synthesizeMouse(content.document.documentElement, 200, 300,
+    {type: "mouseup"}, content.window);
+
+  let bezier = yield onUpdated;
+  is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+  is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
+}
+
+function* curveCanBeClicked(widget) {
+  info("Checking that clicking on the curve moves the closest control point");
+
+  info("Listening for the update event");
+  let onUpdated = widget.once("updated");
+
+  info("Click close to P1");
+  widget._onCurveClick({pageX: 50, pageY: 150});
+
+  let bezier = yield onUpdated;
+  ok(true, "The widget fired the updated event");
+  is(bezier.P1[0], 0.25, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+  is(bezier.P2[0], 1, "P2 time coordinate remained unchanged");
+  is(bezier.P2[1], 0, "P2 progress coordinate remained unchanged");
+
+  info("Listening for the update event");
+  let onUpdated = widget.once("updated");
+
+  info("Click close to P2");
+  widget._onCurveClick({pageX: 150, pageY: 250});
+
+  let bezier = yield onUpdated;
+  is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct");
+  is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
+  is(bezier.P1[0], 0.25, "P1 time coordinate remained unchanged");
+  is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged");
+}
+
+function* pointsCanBeMovedWithKeyboard(widget) {
+  info("Checking that points respond to keyboard events");
+
+  info("Moving P1 to the left");
+  let onUpdated = widget.once("updated");
+  widget._onPointKeyDown(getKeyEvent(widget.p1, 37));
+  let bezier = yield onUpdated;
+  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+  info("Moving P1 to the left, fast");
+  let onUpdated = widget.once("updated");
+  widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true));
+  let bezier = yield onUpdated;
+  is(bezier.P1[0], 0.085, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+  info("Moving P1 to the right, fast");
+  let onUpdated = widget.once("updated");
+  widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true));
+  let bezier = yield onUpdated;
+  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+  info("Moving P1 to the bottom");
+  let onUpdated = widget.once("updated");
+  widget._onPointKeyDown(getKeyEvent(widget.p1, 40));
+  let bezier = yield onUpdated;
+  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
+
+  info("Moving P1 to the bottom, fast");
+  let onUpdated = widget.once("updated");
+  widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true));
+  let bezier = yield onUpdated;
+  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], 0.585, "The new P1 progress coordinate is correct");
+
+  info("Moving P1 to the top, fast");
+  let onUpdated = widget.once("updated");
+  widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true));
+  let bezier = yield onUpdated;
+  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
+
+  info("Checking that keyboard events also work with P2");
+  info("Moving P2 to the left");
+  let onUpdated = widget.once("updated");
+  widget._onPointKeyDown(getKeyEvent(widget.p2, 37));
+  let bezier = yield onUpdated;
+  is(bezier.P2[0], 0.735, "The new P2 time coordinate is correct");
+  is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
+}
+
+function getKeyEvent(target, keyCode, shift=false) {
+  return {
+    target: target,
+    keyCode: keyCode,
+    shiftKey: shift,
+    preventDefault: () => {}
+  };
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_cubic-bezier-03.js
@@ -0,0 +1,67 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that coordinates can be changed programatically in the CubicBezierWidget
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierWidget, PREDEFINED} =
+  devtools.require("devtools/shared/widgets/CubicBezierWidget");
+
+let test = Task.async(function*() {
+  yield promiseTab(TEST_URI);
+
+  let container = content.document.querySelector("#container");
+  let w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+  yield coordinatesCanBeChangedByProvidingAnArray(w);
+  yield coordinatesCanBeChangedByProvidingAValue(w);
+
+  w.destroy();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* coordinatesCanBeChangedByProvidingAnArray(widget) {
+  info("Listening for the update event");
+  let onUpdated = widget.once("updated");
+
+  info("Setting new coordinates");
+  widget.coordinates = [0,1,1,0];
+
+  let bezier = yield onUpdated;
+  ok(true, "The updated event was fired as a result of setting coordinates");
+
+  is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
+  is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+  is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
+}
+
+function* coordinatesCanBeChangedByProvidingAValue(widget) {
+  info("Listening for the update event");
+  let onUpdated = widget.once("updated");
+
+  info("Setting linear css value");
+  widget.cssCubicBezierValue = "linear";
+  let bezier = yield onUpdated;
+  ok(true, "The updated event was fired as a result of setting cssValue");
+
+  is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], 0, "The new P1 progress coordinate is correct");
+  is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+  is(bezier.P2[1], 1, "The new P2 progress coordinate is correct");
+
+  info("Setting a custom cubic-bezier css value");
+  let onUpdated = widget.once("updated");
+  widget.cssCubicBezierValue = "cubic-bezier(.25,-0.5, 1, 1.45)";
+  let bezier = yield onUpdated;
+  ok(true, "The updated event was fired as a result of setting cssValue");
+
+  is(bezier.P1[0], .25, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], -.5, "The new P1 progress coordinate is correct");
+  is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+  is(bezier.P2[1], 1.45, "The new P2 progress coordinate is correct");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/unit/test_bezierCanvas.js
@@ -0,0 +1,113 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the BezierCanvas API in the CubicBezierWidget module
+
+const Cu = Components.utils;
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let require = devtools.require;
+let {CubicBezier, BezierCanvas} = require("devtools/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+  offsetsGetterReturnsData();
+  convertsOffsetsToCoordinates();
+  plotsCanvas();
+}
+
+function offsetsGetterReturnsData() {
+  do_print("offsets getter returns an array of 2 offset objects");
+
+  let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+  let offsets = b.offsets;
+
+  do_check_eq(offsets.length, 2);
+
+  do_check_true("top" in offsets[0]);
+  do_check_true("left" in offsets[0]);
+  do_check_true("top" in offsets[1]);
+  do_check_true("left" in offsets[1]);
+
+  do_check_eq(offsets[0].top, "300px");
+  do_check_eq(offsets[0].left, "0px");
+  do_check_eq(offsets[1].top, "100px");
+  do_check_eq(offsets[1].left, "200px");
+
+  do_print("offsets getter returns data according to current padding");
+
+  let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [0, 0]);
+  let offsets = b.offsets;
+
+  do_check_eq(offsets[0].top, "400px");
+  do_check_eq(offsets[0].left, "0px");
+  do_check_eq(offsets[1].top, "0px");
+  do_check_eq(offsets[1].left, "200px");
+}
+
+function convertsOffsetsToCoordinates() {
+  do_print("Converts offsets to coordinates");
+
+  let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+
+  let coordinates = b.offsetsToCoordinates({style: {
+    left: "0px",
+    top: "0px"
+  }});
+  do_check_eq(coordinates.length, 2);
+  do_check_eq(coordinates[0], 0);
+  do_check_eq(coordinates[1], 1.5);
+
+  let coordinates = b.offsetsToCoordinates({style: {
+    left: "0px",
+    top: "300px"
+  }});
+  do_check_eq(coordinates[0], 0);
+  do_check_eq(coordinates[1], 0);
+
+  let coordinates = b.offsetsToCoordinates({style: {
+    left: "200px",
+    top: "100px"
+  }});
+  do_check_eq(coordinates[0], 1);
+  do_check_eq(coordinates[1], 1);
+}
+
+function plotsCanvas() {
+  do_print("Plots the curve to the canvas");
+
+  let hasDrawnCurve = false;
+  let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+  b.ctx.bezierCurveTo = () => hasDrawnCurve = true;
+  b.plot();
+
+  do_check_true(hasDrawnCurve);
+}
+
+function getCubicBezier() {
+  return new CubicBezier([0,0,1,1]);
+}
+
+function getCanvasMock(w=200, h=400) {
+  return {
+    getContext: function() {
+      return {
+        scale: () => {},
+        translate: () => {},
+        clearRect: () => {},
+        beginPath: () => {},
+        closePath: () => {},
+        moveTo: () => {},
+        lineTo: () => {},
+        stroke: () => {},
+        arc: () => {},
+        fill: () => {},
+        bezierCurveTo: () => {}
+      };
+    },
+    width: w,
+    height: h
+  };
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/unit/test_cubicBezier.js
@@ -0,0 +1,102 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the CubicBezier API in the CubicBezierWidget module
+
+const Cu = Components.utils;
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let require = devtools.require;
+let {CubicBezier} = require("devtools/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+  throwsWhenMissingCoordinates();
+  throwsWhenIncorrectCoordinates();
+  convertsStringCoordinates();
+  coordinatesToStringOutputsAString();
+  pointGettersReturnPointCoordinatesArrays();
+  toStringOutputsCubicBezierValue();
+}
+
+function throwsWhenMissingCoordinates() {
+  do_check_throws(() => {
+    new CubicBezier();
+  }, "Throws an exception when coordinates are missing");
+}
+
+function throwsWhenIncorrectCoordinates() {
+  do_check_throws(() => {
+    new CubicBezier([]);
+  }, "Throws an exception when coordinates are incorrect (empty array)");
+
+  do_check_throws(() => {
+    new CubicBezier([0,0]);
+  }, "Throws an exception when coordinates are incorrect (incomplete array)");
+
+  do_check_throws(() => {
+    new CubicBezier(["a", "b", "c", "d"]);
+  }, "Throws an exception when coordinates are incorrect (invalid type)");
+
+  do_check_throws(() => {
+    new CubicBezier([1.5, 0, 1.5, 0]);
+  }, "Throws an exception when coordinates are incorrect (time range invalid)");
+
+  do_check_throws(() => {
+    new CubicBezier([-0.5, 0, -0.5, 0]);
+  }, "Throws an exception when coordinates are incorrect (time range invalid)");
+}
+
+function convertsStringCoordinates() {
+  do_print("Converts string coordinates to numbers");
+  let c = new CubicBezier(["0", "1", ".5", "-2"]);
+
+  do_check_eq(c.coordinates[0], 0);
+  do_check_eq(c.coordinates[1], 1);
+  do_check_eq(c.coordinates[2], .5);
+  do_check_eq(c.coordinates[3], -2);
+}
+
+function coordinatesToStringOutputsAString() {
+  do_print("coordinates.toString() outputs a string representation");
+
+  let c = new CubicBezier(["0", "1", "0.5", "-2"]);
+  let string = c.coordinates.toString();
+  do_check_eq(string, "0,1,.5,-2");
+
+  let c = new CubicBezier([1, 1, 1, 1]);
+  let string = c.coordinates.toString();
+  do_check_eq(string, "1,1,1,1");
+}
+
+function pointGettersReturnPointCoordinatesArrays() {
+  do_print("Points getters return arrays of coordinates");
+
+  let c = new CubicBezier([0, .2, .5, 1]);
+  do_check_eq(c.P1[0], 0);
+  do_check_eq(c.P1[1], .2);
+  do_check_eq(c.P2[0], .5);
+  do_check_eq(c.P2[1], 1);
+}
+
+function toStringOutputsCubicBezierValue() {
+  do_print("toString() outputs the cubic-bezier() value");
+
+  let c = new CubicBezier([0, 0, 1, 1]);
+  do_check_eq(c.toString(), "cubic-bezier(0,0,1,1)");
+}
+
+function do_check_throws(cb, info) {
+  do_print(info);
+
+  let hasThrown = false;
+  try {
+    cb();
+  } catch (e) {
+    hasThrown = true;
+  }
+
+  do_check_true(hasThrown);
+}
--- a/browser/devtools/shared/test/unit/xpcshell.ini
+++ b/browser/devtools/shared/test/unit/xpcshell.ini
@@ -1,6 +1,8 @@
 [DEFAULT]
 head =
 tail =
 firefox-appdir = browser
 
+[test_bezierCanvas.js]
+[test_cubicBezier.js]
 [test_undoStack.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/CubicBezierWidget.js
@@ -0,0 +1,556 @@
+/**
+ * Copyright (c) 2013 Lea Verou. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+// Based on www.cubic-bezier.com by Lea Verou
+// See https://github.com/LeaVerou/cubic-bezier
+
+"use strict";
+
+const EventEmitter = require("devtools/toolkit/event-emitter");
+const {setTimeout, clearTimeout} = require("sdk/timers");
+
+const PREDEFINED = exports.PREDEFINED = {
+  "ease": [.25, .1, .25, 1],
+  "linear": [0, 0, 1, 1],
+  "ease-in": [.42, 0, 1, 1],
+  "ease-out": [0, 0, .58, 1],
+  "ease-in-out": [.42, 0, .58, 1]
+};
+
+/**
+ * CubicBezier data structure helper
+ * Accepts an array of coordinates and exposes a few useful getters
+ * @param {Array} coordinates i.e. [.42, 0, .58, 1]
+ */
+function CubicBezier(coordinates) {
+  if (!coordinates) {
+    throw "No offsets were defined";
+  }
+
+  this.coordinates = coordinates.map(n => +n);
+
+  for (let i = 4; i--;) {
+    let xy = this.coordinates[i];
+    if (isNaN(xy) || (!(i%2) && (xy < 0 || xy > 1))) {
+      throw "Wrong coordinate at " + i + "(" + xy + ")";
+    }
+  }
+
+  this.coordinates.toString = function() {
+    return this.map(n => {
+      return (Math.round(n * 100)/100 + '').replace(/^0\./, '.');
+    }) + "";
+  }
+}
+
+exports.CubicBezier = CubicBezier;
+
+CubicBezier.prototype = {
+  get P1() {
+    return this.coordinates.slice(0, 2);
+  },
+
+  get P2() {
+    return this.coordinates.slice(2);
+  },
+
+  toString: function() {
+    return 'cubic-bezier(' + this.coordinates + ')';
+  }
+};
+
+/**
+ * Bezier curve canvas plotting class
+ * @param {DOMNode} canvas
+ * @param {CubicBezier} bezier
+ * @param {Array} padding Amount of horizontal,vertical padding around the graph
+ */
+function BezierCanvas(canvas, bezier, padding) {
+  this.canvas = canvas;
+  this.bezier = bezier;
+  this.padding = getPadding(padding);
+
+  // Convert to a cartesian coordinate system with axes from 0 to 1
+  this.ctx = this.canvas.getContext('2d');
+  let p = this.padding;
+
+  this.ctx.scale(canvas.width * (1 - p[1] - p[3]),
+                 -canvas.height * (1 - p[0] - p[2]));
+  this.ctx.translate(p[3] / (1 - p[1] - p[3]),
+                     -1 - p[0] / (1 - p[0] - p[2]));
+};
+
+exports.BezierCanvas = BezierCanvas;
+
+BezierCanvas.prototype = {
+  /**
+   * Get P1 and P2 current top/left offsets so they can be positioned
+   * @return {Array} Returns an array of 2 {top:String,left:String} objects
+   */
+  get offsets() {
+    let p = this.padding, w = this.canvas.width, h = this.canvas.height;
+
+    return [{
+      left: w * (this.bezier.coordinates[0] * (1 - p[3] - p[1]) - p[3]) + 'px',
+      top: h * (1 - this.bezier.coordinates[1] * (1 - p[0] - p[2]) - p[0]) + 'px'
+    }, {
+      left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + 'px',
+      top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0]) + 'px'
+    }]
+  },
+
+  /**
+   * Convert an element's left/top offsets into coordinates
+   */
+  offsetsToCoordinates: function(element) {
+    let p = this.padding, w = this.canvas.width, h = this.canvas.height;
+
+    // Convert padding percentage to actual padding
+    p = p.map(function(a, i) { return a * (i % 2? w : h)});
+
+    return [
+      (parseInt(element.style.left) - p[3]) / (w + p[1] + p[3]),
+      (h - parseInt(element.style.top) - p[2]) / (h - p[0] - p[2])
+    ];
+  },
+
+  /**
+   * Draw the cubic bezier curve for the current coordinates
+   */
+  plot: function(settings={}) {
+    let xy = this.bezier.coordinates;
+
+    let defaultSettings = {
+      handleColor: '#666',
+      handleThickness: .008,
+      bezierColor: '#4C9ED9',
+      bezierThickness: .015
+    };
+
+    for (let setting in settings) {
+      defaultSettings[setting] = settings[setting];
+    }
+
+    this.ctx.clearRect(-.5,-.5, 2, 2);
+
+    // Draw control handles
+    this.ctx.beginPath();
+    this.ctx.fillStyle = defaultSettings.handleColor;
+    this.ctx.lineWidth = defaultSettings.handleThickness;
+    this.ctx.strokeStyle = defaultSettings.handleColor;
+
+    this.ctx.moveTo(0, 0);
+    this.ctx.lineTo(xy[0], xy[1]);
+    this.ctx.moveTo(1,1);
+    this.ctx.lineTo(xy[2], xy[3]);
+
+    this.ctx.stroke();
+    this.ctx.closePath();
+
+    function circle(ctx, cx, cy, r) {
+      return ctx.beginPath();
+      ctx.arc(cx, cy, r, 0, 2*Math.PI, !1);
+      ctx.closePath();
+    }
+
+    circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness);
+    this.ctx.fill();
+    circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness);
+    this.ctx.fill();
+
+    // Draw bezier curve
+    this.ctx.beginPath();
+    this.ctx.lineWidth = defaultSettings.bezierThickness;
+    this.ctx.strokeStyle = defaultSettings.bezierColor;
+    this.ctx.moveTo(0,0);
+    this.ctx.bezierCurveTo(xy[0], xy[1], xy[2], xy[3], 1,1);
+    this.ctx.stroke();
+    this.ctx.closePath();
+  }
+};
+
+/**
+ * Cubic-bezier widget. Uses the BezierCanvas class to draw the curve and
+ * adds the control points and user interaction
+ * @param {DOMNode} parent The container where the graph should be created
+ * @param {Array} coordinates Coordinates of the curve to be drawn
+ *
+ * Emits "updated" events whenever the curve is changed. Along with the event is
+ * sent a CubicBezier object
+ */
+function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) {
+  this.parent = parent;
+  let {curve, p1, p2} = this._initMarkup();
+
+  this.curve = curve;
+  this.curveBoundingBox = curve.getBoundingClientRect();
+  this.p1 = p1;
+  this.p2 = p2;
+
+  // Create and plot the bezier curve
+  this.bezierCanvas = new BezierCanvas(this.curve,
+    new CubicBezier(coordinates), [.25, 0]);
+  this.bezierCanvas.plot();
+
+  // Place the control points
+  let offsets = this.bezierCanvas.offsets;
+  this.p1.style.left = offsets[0].left;
+  this.p1.style.top = offsets[0].top;
+  this.p2.style.left = offsets[1].left;
+  this.p2.style.top = offsets[1].top;
+
+  this._onPointMouseDown = this._onPointMouseDown.bind(this);
+  this._onPointKeyDown = this._onPointKeyDown.bind(this);
+  this._onCurveClick = this._onCurveClick.bind(this);
+  this._initEvents();
+
+  // Add the timing function previewer
+  this.timingPreview = new TimingFunctionPreviewWidget(parent);
+
+  EventEmitter.decorate(this);
+}
+
+exports.CubicBezierWidget = CubicBezierWidget;
+
+CubicBezierWidget.prototype = {
+  _initMarkup: function() {
+    let doc = this.parent.ownerDocument;
+
+    let plane = doc.createElement("div");
+    plane.className = "coordinate-plane";
+
+    let p1 = doc.createElement("button");
+    p1.className = "control-point";
+    p1.id = "P1";
+    plane.appendChild(p1);
+
+    let p2 = doc.createElement("button");
+    p2.className = "control-point";
+    p2.id = "P2";
+    plane.appendChild(p2);
+
+    let curve = doc.createElement("canvas");
+    curve.setAttribute("height", "400");
+    curve.setAttribute("width", "200");
+    curve.id = "curve";
+    plane.appendChild(curve);
+
+    this.parent.appendChild(plane);
+
+    return {
+      p1: p1,
+      p2: p2,
+      curve: curve
+    }
+  },
+
+  _removeMarkup: function() {
+    this.parent.ownerDocument.querySelector(".coordinate-plane").remove();
+  },
+
+  _initEvents: function() {
+    this.p1.addEventListener("mousedown", this._onPointMouseDown);
+    this.p2.addEventListener("mousedown", this._onPointMouseDown);
+
+    this.p1.addEventListener("keydown", this._onPointKeyDown);
+    this.p2.addEventListener("keydown", this._onPointKeyDown);
+
+    this.curve.addEventListener("click", this._onCurveClick);
+  },
+
+  _removeEvents: function() {
+    this.p1.removeEventListener("mousedown", this._onPointMouseDown);
+    this.p2.removeEventListener("mousedown", this._onPointMouseDown);
+
+    this.p1.removeEventListener("keydown", this._onPointKeyDown);
+    this.p2.removeEventListener("keydown", this._onPointKeyDown);
+
+    this.curve.removeEventListener("click", this._onCurveClick);
+  },
+
+  _onPointMouseDown: function(event) {
+    // Updating the boundingbox in case it has changed
+    this.curveBoundingBox = this.curve.getBoundingClientRect();
+
+    let point = event.target;
+    let doc = point.ownerDocument;
+    let self = this;
+
+    doc.onmousemove = function drag(e) {
+      let x = e.pageX;
+      let y = e.pageY;
+      let left = self.curveBoundingBox.left;
+      let top = self.curveBoundingBox.top;
+
+      if (x === 0 && y == 0) {
+        return;
+      }
+
+      // Constrain x
+      x = Math.min(Math.max(left, x), left + self.curveBoundingBox.width);
+
+      point.style.left = x - left + "px";
+      point.style.top = y - top + "px";
+
+      self._updateFromPoints();
+    };
+
+    doc.onmouseup = function () {
+      point.focus();
+      doc.onmousemove = doc.onmouseup = null;
+    }
+  },
+
+  _onPointKeyDown: function(event) {
+    let point = event.target;
+    let code = event.keyCode;
+
+    if (code >= 37 && code <= 40) {
+      event.preventDefault();
+
+      // Arrow keys pressed
+      let left = parseInt(point.style.left);
+      let top = parseInt(point.style.top);
+      let offset = 3 * (event.shiftKey ? 10 : 1);
+
+      switch (code) {
+        case 37: point.style.left = left - offset + 'px'; break;
+        case 38: point.style.top = top - offset + 'px'; break;
+        case 39: point.style.left = left + offset + 'px'; break;
+        case 40: point.style.top = top + offset + 'px'; break;
+      }
+
+      this._updateFromPoints();
+    }
+  },
+
+  _onCurveClick: function(event) {
+    let left = this.curveBoundingBox.left;
+    let top = this.curveBoundingBox.top;
+    let x = event.pageX - left;
+    let y = event.pageY - top;
+
+    // Find which point is closer
+    let distP1 = distance(x, y,
+      parseInt(this.p1.style.left), parseInt(this.p1.style.top));
+    let distP2 = distance(x, y,
+      parseInt(this.p2.style.left), parseInt(this.p2.style.top));
+
+    let point = distP1 < distP2 ? this.p1 : this.p2;
+    point.style.left = x + "px";
+    point.style.top = y + "px";
+
+    this._updateFromPoints();
+  },
+
+  /**
+   * Get the current point coordinates and redraw the curve to match
+   */
+  _updateFromPoints: function() {
+    // Get the new coordinates from the point's offsets
+    let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1)
+    coordinates = coordinates.concat(this.bezierCanvas.offsetsToCoordinates(this.p2));
+
+    this._redraw(coordinates);
+  },
+
+  /**
+   * Redraw the curve
+   * @param {Array} coordinates The array of control point coordinates
+   */
+  _redraw: function(coordinates) {
+    // Provide a new CubicBezier to the canvas and plot the curve
+    this.bezierCanvas.bezier = new CubicBezier(coordinates);
+    this.bezierCanvas.plot();
+    this.emit("updated", this.bezierCanvas.bezier);
+
+    this.timingPreview.preview(this.bezierCanvas.bezier + "");
+  },
+
+  /**
+   * Set new coordinates for the control points and redraw the curve
+   * @param {Array} coordinates
+   */
+  set coordinates(coordinates) {
+    this._redraw(coordinates)
+
+    // Move the points
+    let offsets = this.bezierCanvas.offsets;
+    this.p1.style.left = offsets[0].left;
+    this.p1.style.top = offsets[0].top;
+    this.p2.style.left = offsets[1].left;
+    this.p2.style.top = offsets[1].top;
+  },
+
+  /**
+   * Set new coordinates for the control point and redraw the curve
+   * @param {String} value A string value. E.g. "linear", "cubic-bezier(0,0,1,1)"
+   */
+  set cssCubicBezierValue(value) {
+    if (!value) {
+      return;
+    }
+
+    value = value.trim();
+
+    // Try with one of the predefined values
+    let coordinates = PREDEFINED[value];
+
+    // Otherwise parse the coordinates from the cubic-bezier function
+    if (!coordinates && value.startsWith("cubic-bezier")) {
+      coordinates = value.replace(/cubic-bezier|\(|\)/g, "").split(",").map(parseFloat);
+    }
+
+    this.coordinates = coordinates;
+  },
+
+  destroy: function() {
+    this._removeEvents();
+    this._removeMarkup();
+
+    this.timingPreview.destroy();
+
+    this.curve = this.p1 = this.p2 = null;
+  }
+};
+
+/**
+ * The TimingFunctionPreviewWidget animates a dot on a scale with a given
+ * timing-function
+ * @param {DOMNode} parent The container where this widget should go
+ */
+function TimingFunctionPreviewWidget(parent) {
+  this.previousValue = null;
+  this.autoRestartAnimation = null;
+
+  this.parent = parent;
+  this._initMarkup();
+}
+
+TimingFunctionPreviewWidget.prototype = {
+  PREVIEW_DURATION: 1000,
+
+  _initMarkup: function() {
+    let doc = this.parent.ownerDocument;
+
+    let container = doc.createElement("div");
+    container.className = "timing-function-preview";
+
+    this.dot = doc.createElement("div");
+    this.dot.className = "dot";
+    container.appendChild(this.dot);
+
+    let scale = doc.createElement("div");
+    scale.className = "scale";
+    container.appendChild(scale);
+
+    this.parent.appendChild(container);
+  },
+
+  destroy: function() {
+    clearTimeout(this.autoRestartAnimation);
+    this.parent.querySelector(".timing-function-preview").remove();
+    this.parent = this.dot = null;
+  },
+
+  /**
+   * Preview a new timing function. The current preview will only be stopped if
+   * the supplied function value is different from the previous one. If the
+   * supplied function is invalid, the preview will stop.
+   * @param {String} value
+   */
+  preview: function(value) {
+    // Don't restart the preview animation if the value is the same
+    if (value === this.previousValue) {
+      return false;
+    }
+
+    clearTimeout(this.autoRestartAnimation);
+
+    if (isValidTimingFunction(value)) {
+      this.dot.style.animationTimingFunction = value;
+      this.restartAnimation();
+    }
+
+    this.previousValue = value;
+  },
+
+  /**
+   * Re-start the preview animation from the beginning
+   */
+  restartAnimation: function() {
+    // Reset the animation duration in case it was changed
+    this.dot.style.animationDuration = (this.PREVIEW_DURATION * 2) + "ms";
+
+    // Just toggling the class won't do it unless there's a sync reflow
+    this.dot.classList.remove("animate");
+    let w = this.dot.offsetWidth;
+    this.dot.classList.add("animate");
+
+    // Restart it again after a while
+    this.autoRestartAnimation = setTimeout(this.restartAnimation.bind(this),
+      this.PREVIEW_DURATION * 2);
+  }
+};
+
+// Helpers
+
+function getPadding(padding) {
+  let p = typeof padding === 'number'? [padding] : padding;
+
+  if (p.length === 1) {
+    p[1] = p[0];
+  }
+
+  if (p.length === 2) {
+    p[2] = p[0];
+  }
+
+  if (p.length === 3) {
+    p[3] = p[1];
+  }
+
+  return p;
+}
+
+function distance(x1, y1, x2, y2) {
+  return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
+}
+
+/**
+ * Checks whether a string is a valid timing-function value
+ * @param {String} value
+ * @return {Boolean}
+ */
+function isValidTimingFunction(value) {
+  // Either it's a predefined value
+  if (value in PREDEFINED) {
+    return true;
+  }
+
+  // Or it has to match a cubic-bezier expression
+  if (value.match(/^cubic-bezier\(([0-9.\- ]+,){3}[0-9.\- ]+\)/)) {
+    return true;
+  }
+
+  return false;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/cubic-bezier-frame.xhtml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+  <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
+  <link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" ype="text/css"/>
+  <script type="application/javascript;version=1.8" src="theme-switching.js"/>
+  <style>
+    body {
+      margin: 0;
+      padding: 0;
+      width: 200px;
+      height: 415px;
+    }
+  </style>
+</head>
+<body role="application">
+  <div id="container"></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/cubic-bezier.css
@@ -0,0 +1,142 @@
+/* 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/. */
+
+/* Based on Lea Verou www.cubic-bezier.com
+   See https://github.com/LeaVerou/cubic-bezier */
+
+.coordinate-plane {
+  position: absolute;
+  line-height: 0;
+  height: 400px;
+  width: 200px;
+}
+
+.coordinate-plane:before,
+.coordinate-plane:after {
+  position: absolute;
+  bottom: 25%;
+  left: 0;
+  width: 100%;
+}
+
+.coordinate-plane:before {
+  content: "";
+  border-bottom: 2px solid;
+  transform: rotate(-90deg) translateY(2px);
+  transform-origin: bottom left;
+}
+
+.coordinate-plane:after {
+  content: "";
+  border-top: 2px solid;
+  margin-bottom: -2px;
+}
+
+.theme-dark .coordinate-plane:before,
+.theme-dark .coordinate-plane:after {
+  border-color: #eee;
+}
+
+.control-point {
+  position: absolute;
+  z-index: 1;
+  height: 10px;
+  width: 10px;
+  border: 0;
+  background: #666;
+  display: block;
+  margin: -5px 0 0 -5px;
+  outline: none;
+  border-radius: 5px;
+  padding: 0;
+
+  cursor: pointer;
+}
+
+#P1x, #P1y {
+  color: #f08;
+}
+
+#P2x, #P2y {
+  color: #0ab;
+}
+
+canvas#curve {
+  background:
+    linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat,
+    repeating-linear-gradient(transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat,
+    repeating-linear-gradient(-90deg, transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat;
+
+  background-size: 100% 50%, 100% 50%, 100% 50%;
+  background-position: 25%, 0, 0;
+
+  -moz-user-select: none;
+}
+
+.theme-dark canvas#curve {
+  background:
+    linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat,
+    repeating-linear-gradient(transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat,
+    repeating-linear-gradient(-90deg, transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat;
+
+  background-size: 100% 50%, 100% 50%, 100% 50%;
+  background-position: 25%, 0, 0;
+}
+
+/* Timing function preview widget */
+
+.timing-function-preview {
+  position: absolute;
+  top: 400px;
+}
+
+.timing-function-preview .scale {
+  position: absolute;
+  top: 6px;
+  left: 0;
+  z-index: 1;
+
+  width: 200px;
+  height: 1px;
+
+  background: #ccc;
+}
+
+.timing-function-preview .dot {
+  position: absolute;
+  top: 0;
+  left: -7px;
+  z-index: 2;
+
+  width: 10px;
+  height: 10px;
+
+  border-radius: 50%;
+  border: 2px solid white;
+  background: #4C9ED9;
+}
+
+.timing-function-preview .dot.animate {
+  animation-duration: 2.5s;
+  animation-fill-mode: forwards;
+  animation-name: timing-function-preview;
+}
+
+@keyframes timing-function-preview {
+  0% {
+    left: -7px;
+  }
+  33% {
+    left: 193px;
+  }
+  50% {
+    left: 193px;
+  }
+  83% {
+    left: -7px;
+  }
+  100% {
+    left: -7px;
+  }
+}
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -25,8 +25,13 @@ stop=Stop
 peer_ended_conversation=Your peer ended the conversation.
 call_has_ended=Your call has ended.
 close_window=Close this window
 
 cannot_start_call_session_not_ready=Can't start call, session is not ready.
 network_disconnected=The network connection terminated abruptly.
 
 connection_error_see_console_notification=Call failed; see console for details.
+## LOCALIZATION NOTE (legal_text_and_links): In this item, don't translate the
+## part between {{..}}
+legal_text_and_links.innerHTML=By using this product you agree to the <a \
+  href="{{terms_of_use_url}}">Terms of Use</a> and <a \
+  href="{{privacy_notice_url}}">Privacy Notice</a>
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -2090,18 +2090,16 @@ public class GeckoAppShell
             return null;
         }
     }
 
     private static ContextGetter sContextGetter;
 
     @WrapElementForJNI(allowMultithread = true)
     public static Context getContext() {
-        if (sContextGetter == null)
-            return null;
         return sContextGetter.getContext();
     }
 
     public static void setContextGetter(ContextGetter cg) {
         sContextGetter = cg;
     }
 
     public static SharedPreferences getSharedPreferences() {
--- a/mobile/android/base/GeckoApplication.java
+++ b/mobile/android/base/GeckoApplication.java
@@ -97,18 +97,16 @@ public class GeckoApplication extends Ap
                 }
             });
         }
         GeckoConnectivityReceiver.getInstance().stop();
         GeckoNetworkManager.getInstance().stop();
     }
 
     public void onActivityResume(GeckoActivityStatus activity) {
-	GeckoAppShell.setContextGetter(this);
-
         if (mPausedGecko) {
             GeckoAppShell.sendEventToGecko(GeckoEvent.createAppForegroundingEvent());
             mPausedGecko = false;
         }
 
         final Context applicationContext = getApplicationContext();
         GeckoBatteryManager.getInstance().start(applicationContext);
         GeckoConnectivityReceiver.getInstance().start(applicationContext);
--- a/mobile/android/base/home/HomeConfig.java
+++ b/mobile/android/base/home/HomeConfig.java
@@ -101,24 +101,24 @@ public final class HomeConfig {
         private final PanelType mType;
         private final String mTitle;
         private final String mId;
         private final LayoutType mLayoutType;
         private final List<ViewConfig> mViews;
         private final AuthConfig mAuthConfig;
         private final EnumSet<Flags> mFlags;
 
-        private static final String JSON_KEY_TYPE = "type";
-        private static final String JSON_KEY_TITLE = "title";
-        private static final String JSON_KEY_ID = "id";
-        private static final String JSON_KEY_LAYOUT = "layout";
-        private static final String JSON_KEY_VIEWS = "views";
-        private static final String JSON_KEY_AUTH_CONFIG = "authConfig";
-        private static final String JSON_KEY_DEFAULT = "default";
-        private static final String JSON_KEY_DISABLED = "disabled";
+        static final String JSON_KEY_TYPE = "type";
+        static final String JSON_KEY_TITLE = "title";
+        static final String JSON_KEY_ID = "id";
+        static final String JSON_KEY_LAYOUT = "layout";
+        static final String JSON_KEY_VIEWS = "views";
+        static final String JSON_KEY_AUTH_CONFIG = "authConfig";
+        static final String JSON_KEY_DEFAULT = "default";
+        static final String JSON_KEY_DISABLED = "disabled";
 
         public enum Flags {
             DEFAULT_PANEL,
             DISABLED_PANEL
         }
 
         public PanelConfig(JSONObject json) throws JSONException, IllegalArgumentException {
             final String panelType = json.optString(JSON_KEY_TYPE, null);
@@ -611,24 +611,24 @@ public final class HomeConfig {
         private final String mDatasetId;
         private final ItemType mItemType;
         private final ItemHandler mItemHandler;
         private final String mBackImageUrl;
         private final String mFilter;
         private final EmptyViewConfig mEmptyViewConfig;
         private final EnumSet<Flags> mFlags;
 
-        private static final String JSON_KEY_TYPE = "type";
-        private static final String JSON_KEY_DATASET = "dataset";
-        private static final String JSON_KEY_ITEM_TYPE = "itemType";
-        private static final String JSON_KEY_ITEM_HANDLER = "itemHandler";
-        private static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl";
-        private static final String JSON_KEY_FILTER = "filter";
-        private static final String JSON_KEY_EMPTY = "empty";
-        private static final String JSON_KEY_REFRESH_ENABLED = "refreshEnabled";
+        static final String JSON_KEY_TYPE = "type";
+        static final String JSON_KEY_DATASET = "dataset";
+        static final String JSON_KEY_ITEM_TYPE = "itemType";
+        static final String JSON_KEY_ITEM_HANDLER = "itemHandler";
+        static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl";
+        static final String JSON_KEY_FILTER = "filter";
+        static final String JSON_KEY_EMPTY = "empty";
+        static final String JSON_KEY_REFRESH_ENABLED = "refreshEnabled";
 
         public enum Flags {
             REFRESH_ENABLED
         }
 
         public ViewConfig(int index, JSONObject json) throws JSONException, IllegalArgumentException {
             mIndex = index;
             mType = ViewType.fromId(json.getString(JSON_KEY_TYPE));
@@ -813,18 +813,18 @@ public final class HomeConfig {
             }
         };
     }
 
     public static class EmptyViewConfig implements Parcelable {
         private final String mText;
         private final String mImageUrl;
 
-        private static final String JSON_KEY_TEXT = "text";
-        private static final String JSON_KEY_IMAGE_URL = "imageUrl";
+        static final String JSON_KEY_TEXT = "text";
+        static final String JSON_KEY_IMAGE_URL = "imageUrl";
 
         public EmptyViewConfig(JSONObject json) throws JSONException, IllegalArgumentException {
             mText = json.optString(JSON_KEY_TEXT, null);
             mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null);
         }
 
         @SuppressWarnings("unchecked")
         public EmptyViewConfig(Parcel in) {
@@ -883,19 +883,19 @@ public final class HomeConfig {
         };
     }
 
     public static class AuthConfig implements Parcelable {
         private final String mMessageText;
         private final String mButtonText;
         private final String mImageUrl;
 
-        private static final String JSON_KEY_MESSAGE_TEXT = "messageText";
-        private static final String JSON_KEY_BUTTON_TEXT = "buttonText";
-        private static final String JSON_KEY_IMAGE_URL = "imageUrl";
+        static final String JSON_KEY_MESSAGE_TEXT = "messageText";
+        static final String JSON_KEY_BUTTON_TEXT = "buttonText";
+        static final String JSON_KEY_IMAGE_URL = "imageUrl";
 
         public AuthConfig(JSONObject json) throws JSONException, IllegalArgumentException {
             mMessageText = json.optString(JSON_KEY_MESSAGE_TEXT);
             mButtonText = json.optString(JSON_KEY_BUTTON_TEXT);
             mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null);
         }
 
         @SuppressWarnings("unchecked")
--- a/mobile/android/base/home/HomeConfigPrefsBackend.java
+++ b/mobile/android/base/home/HomeConfigPrefsBackend.java
@@ -93,16 +93,32 @@ class HomeConfigPrefsBackend implements 
             panelConfigs.add(0, historyEntry);
             panelConfigs.add(0, recentTabsEntry);
         }
 
         return new State(panelConfigs, true);
     }
 
     /**
+     * Iterate through the panels to check if they are all disabled.
+     */
+    private static boolean allPanelsAreDisabled(JSONArray jsonPanels) throws JSONException {
+        final int count = jsonPanels.length();
+        for (int i = 0; i < count; i++) {
+            final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
+
+            if (!jsonPanelConfig.optBoolean(PanelConfig.JSON_KEY_DISABLED, false)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
      * Migrates JSON config data storage.
      *
      * @param context Context used to get shared preferences and create built-in panel.
      * @param jsonString String currently stored in preferences.
      *
      * @return JSONArray array representing new set of panel configs.
      */
     private static synchronized JSONArray maybePerformMigration(Context context, String jsonString) throws JSONException {
@@ -139,18 +155,23 @@ class HomeConfigPrefsBackend implements 
         final SharedPreferences.Editor prefsEditor = prefs.edit();
 
         for (int v = version + 1; v <= VERSION; v++) {
             Log.d(LOGTAG, "Migrating to version = " + v);
 
             switch (v) {
                 case 1:
                     // Add "Recent Tabs" panel
-                    final PanelConfig recentTabsConfig = createBuiltinPanelConfig(context, PanelType.RECENT_TABS);
-                    final JSONObject jsonRecentTabsConfig = recentTabsConfig.toJSON();
+                    final JSONObject jsonRecentTabsConfig =
+                            createBuiltinPanelConfig(context, PanelType.RECENT_TABS).toJSON();
+
+                    // If any panel is enabled, then we should make the recent tabs
+                    // panel enabled.
+                    jsonRecentTabsConfig.put(PanelConfig.JSON_KEY_DISABLED,
+                                             allPanelsAreDisabled(originalJsonPanels));
 
                     // Add the new panel to the front of the array on phones.
                     if (!HardwareUtils.isTablet()) {
                         newJsonPanels.put(jsonRecentTabsConfig);
                     }
 
                     // Copy the original panel configs.
                     final int count = originalJsonPanels.length();
--- a/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml
+++ b/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml
@@ -41,21 +41,20 @@
                                               gecko:tabs="tabs_normal"/>
 
         <org.mozilla.gecko.tabspanel.PrivateTabsPanel
                 android:id="@+id/private_tabs_panel"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:visibility="gone"/>
 
-        <org.mozilla.gecko.tabspanel.RemoteTabsPanel
-                android:id="@+id/remote_tabs"
-                android:layout_height="match_parent"
-                android:layout_width="match_parent"
-                android:visibility="gone"/>
+        <ViewStub android:id="@+id/remote_tabs_panel_stub"
+                  android:layout="@layout/remote_tabs_panel_view"
+                  android:layout_width="match_parent"
+                  android:layout_height="match_parent"/>
 
     </view>
 
     <RelativeLayout android:id="@+id/tabs_panel_footer"
                     android:layout_width="match_parent"
                     android:layout_height="@dimen/browser_toolbar_height">
 
         <view class="org.mozilla.gecko.tabspanel.TabsPanel$TabsPanelToolbar"
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/remote_tabs_panel_view.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<org.mozilla.gecko.tabspanel.RemoteTabsPanel xmlns:android="http://schemas.android.com/apk/res/android"
+                                             android:id="@+id/remote_tabs_panel"
+                                             android:layout_width="match_parent"
+                                             android:layout_height="match_parent"/>
--- a/mobile/android/base/resources/layout/tabs_panel.xml
+++ b/mobile/android/base/resources/layout/tabs_panel.xml
@@ -40,17 +40,16 @@
                                               gecko:tabs="tabs_normal"/>
 
         <org.mozilla.gecko.tabspanel.PrivateTabsPanel
                 android:id="@+id/private_tabs_panel"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:visibility="gone"/>
 
-        <org.mozilla.gecko.tabspanel.RemoteTabsPanel
-                android:id="@+id/remote_tabs"
-                android:layout_height="match_parent"
-                android:layout_width="match_parent"
-                android:visibility="gone"/>
 
+        <ViewStub android:id="@+id/remote_tabs_panel_stub"
+                  android:layout="@layout/remote_tabs_panel_view"
+                  android:layout_width="match_parent"
+                  android:layout_height="match_parent"/>
     </view>
 
 </merge>
--- a/mobile/android/base/tabspanel/TabsPanel.java
+++ b/mobile/android/base/tabspanel/TabsPanel.java
@@ -26,16 +26,17 @@ import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
+import android.view.ViewStub;
 import android.widget.Button;
 import android.widget.FrameLayout;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
 
 public class TabsPanel extends LinearLayout
                        implements GeckoPopupMenu.OnMenuItemClickListener,
@@ -112,16 +113,17 @@ public class TabsPanel extends LinearLay
         LayoutInflater.from(context).inflate(R.layout.tabs_panel, this);
         initialize();
 
         mAppStateListener = new AppStateListener() {
             @Override
             public void onResume() {
                 if (mPanel == mPanelRemote) {
                     // Refresh the remote panel.
+                    initializeRemotePanelView();
                     mPanelRemote.show();
                 }
             }
 
             @Override
             public void onOrientationChanged() {
                 // Remote panel is already refreshed by chrome refresh.
             }
@@ -136,19 +138,16 @@ public class TabsPanel extends LinearLay
         mTabsContainer = (TabsListContainer) findViewById(R.id.tabs_container);
 
         mPanelNormal = (PanelView) findViewById(R.id.normal_tabs);
         mPanelNormal.setTabsPanel(this);
 
         mPanelPrivate = (PanelView) findViewById(R.id.private_tabs_panel);
         mPanelPrivate.setTabsPanel(this);
 
-        mPanelRemote = (PanelView) findViewById(R.id.remote_tabs);
-        mPanelRemote.setTabsPanel(this);
-
         mFooter = (RelativeLayout) findViewById(R.id.tabs_panel_footer);
 
         mAddTab = (ImageButton) findViewById(R.id.add_tab);
         mAddTab.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View v) {
                 TabsPanel.this.addTab();
             }
@@ -402,16 +401,17 @@ public class TabsPanel extends LinearLay
         switch (panelToShow) {
             case NORMAL_TABS:
                 mPanel = mPanelNormal;
                 break;
             case PRIVATE_TABS:
                 mPanel = mPanelPrivate;
                 break;
             case REMOTE_TABS:
+                initializeRemotePanelView();
                 mPanel = mPanelRemote;
                 break;
 
             default:
                 throw new IllegalArgumentException("Unknown panel type " + panelToShow);
         }
         mPanel.show();
 
@@ -550,9 +550,16 @@ public class TabsPanel extends LinearLay
      */
     public Drawable getIconDrawable(Panel panel) {
         return mTabWidget.getIconDrawable(panel.ordinal());
     }
 
     public void setIconDrawable(Panel panel, int resource) {
         mTabWidget.setIconDrawable(panel.ordinal(), resource);
     }
+
+    private void initializeRemotePanelView() {
+        if (mPanelRemote == null) {
+            mPanelRemote = (PanelView) ((ViewStub) findViewById(R.id.remote_tabs_panel_stub)).inflate();
+            mPanelRemote.setTabsPanel(TabsPanel.this);
+        }
+    }
 }
--- a/toolkit/content/customizeToolbar.js
+++ b/toolkit/content/customizeToolbar.js
@@ -4,16 +4,18 @@
 
 var gToolboxDocument = null;
 var gToolbox = null;
 var gCurrentDragOverItem = null;
 var gToolboxChanged = false;
 var gToolboxSheet = false;
 var gPaletteBox = null;
 
+Components.utils.import("resource://gre/modules/Services.jsm");
+
 function onLoad()
 {
   if ("arguments" in window && window.arguments[0]) {
     InitWithToolbox(window.arguments[0]);
     repositionDialog(window);
   }
   else if (window.frameElement &&
            "toolbox" in window.frameElement) {
@@ -452,19 +454,17 @@ function setDragActive(aItem, aValue)
       node.setAttribute("dragover", value);
   } else {
     node.removeAttribute("dragover");
   }
 }
 
 function addNewToolbar()
 {
-  var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
-                                .getService(Components.interfaces.nsIPromptService);
-
+  var promptService = Services.prompt;
   var stringBundle = document.getElementById("stringBundle");
   var message = stringBundle.getString("enterToolbarName");
   var title = stringBundle.getString("enterToolbarTitle");
 
   var name = {};
 
   // Quitting from the toolbar dialog while the new toolbar prompt is up
   // can cause things to become unresponsive on the Mac. Until dialog modality
@@ -814,16 +814,22 @@ function onPaletteDrop(aEvent)
     wrapper.parentNode.removeChild(wrapper);
   }
 
   toolboxChanged();
 }
 
 
 function isUnwantedDragEvent(aEvent) {
+  try {
+    if (Services.prefs.getBoolPref("toolkit.customization.unsafe_drag_events")) {
+      return false;
+    }
+  } catch (ex) {}
+
   /* Discard drag events that originated from a separate window to
      prevent content->chrome privilege escalations. */
   let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
   // mozSourceNode is null in the dragStart event handler or if
   // the drag event originated in an external application.
   if (!mozSourceNode) {
     return true;
   }
--- a/toolkit/content/license.html
+++ b/toolkit/content/license.html
@@ -71,16 +71,17 @@
       <li><a href="about:license#apple">Apple License</a></li>
       <li><a href="about:license#apple-mozilla">Apple/Mozilla NPRuntime License</a></li>
       <li><a href="about:license#apple-torch">Apple/Torch Mobile License</a></li>
       <li><a href="about:license#backbone">Backbone License</a></li>
       <li><a href="about:license#bspatch">bspatch License</a></li>
       <li><a href="about:license#cairo">Cairo Component Licenses</a></li>
       <li><a href="about:license#chromium">Chromium License</a></li>
       <li><a href="about:license#codemirror">CodeMirror License</a></li>
+      <li><a href="about:license#cubic-bezier">cubic-bezier License</a></li>
       <li><a href="about:license#dtoa">dtoa License</a></li>
       <li><a href="about:license#hunspell-nl">Dutch Spellchecking Dictionary License</a></li>
       <li><a href="about:license#edl">Eclipse Distribution License</a></li>
       <li><a href="about:license#escodegen">Escodegen License</a></li>
       <li><a href="about:license#hunspell-ee">Estonian Spellchecking Dictionary License</a></li>
       <li><a href="about:license#expat">Expat License</a></li>
       <li><a href="about:license#firebug">Firebug License</a></li>
       <li><a href="about:license#gfx-font-list">gfxFontList License</a></li>
@@ -2023,16 +2024,46 @@ THE SOFTWARE.
 Please note that some subdirectories of the CodeMirror distribution
 include their own LICENSE files, and are released under different
 licences.
 </pre>
 
 
     <hr>
 
+    <h1><a id="cubic-bezier"></a>cubic-bezier License</h1>
+
+    <p>This license applies to the file
+    <span class="path">browser/devtools/shared/widgets/CubicBezierWidget.js
+    </span>.</p>
+<pre>
+Copyright (c) 2013 Lea Verou. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+</pre>
+
+
+    <hr>
+
     <h1><a id="dtoa"></a>dtoa License</h1>
 
     <p>This license applies to the file
     <span class="path">nsprpub/pr/src/misc/dtoa.c</span>.</p>
 
 <pre>
 The author of this software is David M. Gay.
 
--- a/toolkit/devtools/gcli/source/lib/gcli/cli.js
+++ b/toolkit/devtools/gcli/source/lib/gcli/cli.js
@@ -487,16 +487,20 @@ function Requisition(options) {
   this._nextUpdateId = 0;
 
   // We can set a prefix to typed commands to make it easier to focus on
   // Allowing us to type "add -a; commit" in place of "git add -a; git commit"
   this.prefix = '';
 
   addMapping(this);
   this._setBlankAssignment(this.commandAssignment);
+
+  // If a command calls context.update then the UI needs some way to be
+  // informed of the change
+  this.onExternalUpdate = util.createEvent('Requisition.onExternalUpdate');
 }
 
 /**
  * Avoid memory leaks
  */
 Requisition.prototype.destroy = function() {
   this.document = undefined;
   this.environment = undefined;
@@ -579,18 +583,18 @@ Object.defineProperty(Requisition.protot
       Object.defineProperty(this._executionContext, 'shell', {
         get: function() { return requisition.shell; },
         enumerable : true
       });
 
       if (legacy) {
         this._executionContext.createView = view.createView;
         this._executionContext.exec = this.exec.bind(this);
-        this._executionContext.update = this.update.bind(this);
-        this._executionContext.updateExec = this.updateExec.bind(this);
+        this._executionContext.update = this._contextUpdate.bind(this);
+        this._executionContext.updateExec = this._contextUpdateExec.bind(this);
 
         Object.defineProperty(this._executionContext, 'document', {
           get: function() { return requisition.document; },
           enumerable: true
         });
       }
     }
 
@@ -607,18 +611,18 @@ Object.defineProperty(Requisition.protot
     if (this._conversionContext == null) {
       this._conversionContext = {
         defer: function() {
           return Promise.defer();
         },
 
         createView: view.createView,
         exec: this.exec.bind(this),
-        update: this.update.bind(this),
-        updateExec: this.updateExec.bind(this)
+        update: this._contextUpdate.bind(this),
+        updateExec: this._contextUpdateExec.bind(this)
       };
 
       // Alias requisition so we're clear about what's what
       var requisition = this;
 
       Object.defineProperty(this._conversionContext, 'document', {
         get: function() { return requisition.document; },
         enumerable: true
@@ -762,26 +766,45 @@ Requisition.prototype._getFirstBlankPosi
       return true; // i.e. break
     }
     return false;
   }, this);
   return reply;
 };
 
 /**
+ * The update process is asynchronous, so there is (unavoidably) a window
+ * where we've worked out the command but don't yet understand all the params.
+ * If we try to do things to a requisition in this window we may get
+ * inconsistent results. Asynchronous promises have made the window bigger.
+ * The only time we've seen this in practice is during focus events due to
+ * clicking on a shortcut. The focus want to check the cursor position while
+ * the shortcut is updating the command line.
+ * This function allows us to detect and back out of this problem.
+ * We should be able to remove this function when all the state in a
+ * requisition can be encapsulated and updated atomically.
+ */
+Requisition.prototype.isUpToDate = function() {
+  if (!this._args) {
+    return false;
+  }
+  for (var i = 0; i < this._args.length; i++) {
+    if (this._args[i].assignment == null) {
+      return false;
+    }
+  }
+  return true;
+};
+
+/**
  * Look through the arguments attached to our assignments for the assignment
  * at the given position.
  * @param {number} cursor The cursor position to query
  */
 Requisition.prototype.getAssignmentAt = function(cursor) {
-  if (!this._args) {
-    console.trace();
-    throw new Error('Missing args');
-  }
-
   // We short circuit this one because we may have no args, or no args with
   // any size and the alg below only finds arguments with size.
   if (cursor === 0) {
     return this.commandAssignment;
   }
 
   var assignForPos = [];
   var i, j;
@@ -817,24 +840,17 @@ Requisition.prototype.getAssignmentAt = 
     for (j = 0; j < arg.suffix.length; j++) {
       assignForPos.push(assignment);
     }
   }
 
   // Possible shortcut, we don't really need to go through all the args
   // to work out the solution to this
 
-  var reply = assignForPos[cursor - 1];
-
-  if (!reply) {
-    throw new Error('Missing assignment.' +
-        ' cursor=' + cursor + ' text=' + this.toString());
-  }
-
-  return reply;
+  return assignForPos[cursor - 1];
 };
 
 /**
  * Extract a canonical version of the input
  * @return a promise of a string which is the canonical version of what was
  * typed
  */
 Requisition.prototype.toCanonicalString = function() {
@@ -1474,24 +1490,40 @@ function getDataCommandAttribute(element
   if (!command) {
     command = element.querySelector('*[data-command]')
                      .getAttribute('data-command');
   }
   return command;
 }
 
 /**
+ * Designed to be called from context.update(). Acts just like update() except
+ * that it also calls onExternalUpdate() to inform the UI of an unexpected
+ * change to the current command.
+ */
+Requisition.prototype._contextUpdate = function(typed) {
+  return this.update(typed).then(function(reply) {
+    this.onExternalUpdate({ typed: typed });
+    return reply;
+  }.bind(this));
+};
+
+/**
  * Called by the UI when ever the user interacts with a command line input
- * @param typed The contents of the input field
+ * @param typed The contents of the input field OR an HTML element (or an event
+ * that targets an HTML element) which has a data-command attribute or a child
+ * with the same that contains the command to update with
  */
 Requisition.prototype.update = function(typed) {
-  if (typeof HTMLElement !== 'undefined' && typed instanceof HTMLElement) {
+  // Should be "if (typed instanceof HTMLElement)" except Gecko
+  if (typeof typed.querySelector === 'function') {
     typed = getDataCommandAttribute(typed);
   }
-  if (typeof Event !== 'undefined' && typed instanceof Event) {
+  // Should be "if (typed instanceof Event)" except Gecko
+  if (typeof typed.currentTarget === 'object') {
     typed = getDataCommandAttribute(typed.currentTarget);
   }
 
   var updateId = this._beginChange();
 
   this._args = exports.tokenize(typed);
   var args = this._args.slice(0); // i.e. clone
 
@@ -2064,16 +2096,28 @@ Requisition.prototype.exec = function(op
     }
     finally {
       this.clear();
     }
   }
 };
 
 /**
+ * Designed to be called from context.updateExec(). Acts just like updateExec()
+ * except that it also calls onExternalUpdate() to inform the UI of an
+ * unexpected change to the current command.
+ */
+Requisition.prototype._contextUpdateExec = function(typed, options) {
+  return this.updateExec(typed, options).then(function(reply) {
+    this.onExternalUpdate({ typed: typed });
+    return reply;
+  }.bind(this));
+};
+
+/**
  * A shortcut for calling update, resolving the promise and then exec.
  * @param input The string to execute
  * @param options Passed to exec
  * @return A promise of an output object
  */
 Requisition.prototype.updateExec = function(input, options) {
   return this.update(input).then(function() {
     return this.exec(options);
--- a/toolkit/devtools/gcli/source/lib/gcli/languages/command.js
+++ b/toolkit/devtools/gcli/source/lib/gcli/languages/command.js
@@ -101,25 +101,28 @@ var commandLanguage = exports.commandLan
 
     return commandHtmlPromise.then(function(commandHtml) {
       this.commandDom = host.toDom(this.document, commandHtml);
 
       this.requisition.commandOutputManager.onOutput.add(this.outputted, this);
       var mapping = cli.getMapping(this.requisition.executionContext);
       mapping.terminal = this.terminal;
 
+      this.requisition.onExternalUpdate.add(this.textChanged, this);
+
       return this;
     }.bind(this));
   },
 
   destroy: function() {
     var mapping = cli.getMapping(this.requisition.executionContext);
     delete mapping.terminal;
 
     this.requisition.commandOutputManager.onOutput.remove(this.outputted, this);
+    this.requisition.onExternalUpdate.remove(this.textChanged, this);
 
     this.terminal = undefined;
     this.requisition = undefined;
     this.commandDom = undefined;
   },
 
   // From the requisition.textChanged event
   textChanged: function() {
@@ -158,17 +161,24 @@ var commandLanguage = exports.commandLan
     this.terminal.field.update();
     this.terminal.field.setConversion(this.assignment.conversion);
     util.setTextContent(this.terminal.descriptionEle, this.description);
   },
 
   // Called internally whenever we think that the current assignment might
   // have changed, typically on mouse-clicks or key presses.
   caretMoved: function(start) {
+    if (!this.requisition.isUpToDate()) {
+      return;
+    }
     var newAssignment = this.requisition.getAssignmentAt(start);
+    if (newAssignment == null) {
+      return;
+    }
+
     if (this.assignment !== newAssignment) {
       if (this.assignment.param.type.onLeave) {
         this.assignment.param.type.onLeave(this.assignment);
       }
 
       // This can be kicked off either by requisition doing an assign or by
       // terminal noticing a cursor movement out of a command, so we should
       // check that this really is a new assignment
--- a/toolkit/devtools/gcli/source/lib/gcli/mozui/inputter.js
+++ b/toolkit/devtools/gcli/source/lib/gcli/mozui/inputter.js
@@ -82,28 +82,30 @@ function Inputter(options, components) {
 
   this.assignment = this.requisition.getAssignmentAt(0);
   this.onAssignmentChange = util.createEvent('Inputter.onAssignmentChange');
   this.onInputChange = util.createEvent('Inputter.onInputChange');
 
   this.onResize = util.createEvent('Inputter.onResize');
   this.onWindowResize = this.onWindowResize.bind(this);
   this.document.defaultView.addEventListener('resize', this.onWindowResize, false);
+  this.requisition.onExternalUpdate.add(this.textChanged, this);
 
   this._previousValue = undefined;
   this.requisition.update(this.element.value || '');
 }
 
 /**
  * Avoid memory leaks
  */
 Inputter.prototype.destroy = function() {
   this.document.defaultView.removeEventListener('resize', this.onWindowResize, false);
 
   this.requisition.commandOutputManager.onOutput.remove(this.outputted, this);
+  this.requisition.onExternalUpdate.remove(this.textChanged, this);
   if (this.focusManager) {
     this.focusManager.removeMonitoredElement(this.element, 'input');
   }
 
   this.element.removeEventListener('mouseup', this.onMouseUp, false);
   this.element.removeEventListener('keydown', this.onKeyDown, false);
   this.element.removeEventListener('keyup', this.onKeyUp, false);
 
@@ -304,17 +306,23 @@ Inputter.prototype._processCaretChange =
  * @param start Optional - if specified, the cursor position to use in working
  * out the current assignment. This is needed because setting the element
  * selection start is only recognised when the event loop has finished
  */
 Inputter.prototype._checkAssignment = function(start) {
   if (start == null) {
     start = this.element.selectionStart;
   }
+  if (!this.requisition.isUpToDate()) {
+    return;
+  }
   var newAssignment = this.requisition.getAssignmentAt(start);
+  if (newAssignment == null) {
+    return;
+  }
   if (this.assignment !== newAssignment) {
     if (this.assignment.param.type.onLeave) {
       this.assignment.param.type.onLeave(this.assignment);
     }
 
     this.assignment = newAssignment;
     this.onAssignmentChange({ assignment: this.assignment });
 
--- a/toolkit/devtools/server/actors/call-watcher.js
+++ b/toolkit/devtools/server/actors/call-watcher.js
@@ -271,26 +271,27 @@ let CallWatcherActor = exports.CallWatch
     this.finalize();
   },
 
   /**
    * Starts waiting for the current tab actor's document global to be
    * created, in order to instrument the specified objects and become
    * aware of everything the content does with them.
    */
-  setup: method(function({ tracedGlobals, tracedFunctions, startRecording, performReload, holdWeak }) {
+  setup: method(function({ tracedGlobals, tracedFunctions, startRecording, performReload, holdWeak, storeCalls }) {
     if (this._initialized) {
       return;
     }
     this._initialized = true;
 
     this._functionCalls = [];
     this._tracedGlobals = tracedGlobals || [];
     this._tracedFunctions = tracedFunctions || [];
     this._holdWeak = !!holdWeak;
+    this._storeCalls = !!storeCalls;
     this._contentObserver = new ContentObserver(this.tabActor);
 
     on(this._contentObserver, "global-created", this._onGlobalCreated);
     on(this._contentObserver, "global-destroyed", this._onGlobalDestroyed);
 
     if (startRecording) {
       this.resumeRecording();
     }
@@ -298,17 +299,18 @@ let CallWatcherActor = exports.CallWatch
       this.tabActor.window.location.reload();
     }
   }, {
     request: {
       tracedGlobals: Option(0, "nullable:array:string"),
       tracedFunctions: Option(0, "nullable:array:string"),
       startRecording: Option(0, "boolean"),
       performReload: Option(0, "boolean"),
-      holdWeak: Option(0, "boolean")
+      holdWeak: Option(0, "boolean"),
+      storeCalls: Option(0, "boolean")
     },
     oneway: true
   }),
 
   /**
    * Stops listening for document global changes and puts this actor
    * to hibernation. This method is called automatically just before the
    * actor is destroyed.
@@ -536,28 +538,31 @@ let CallWatcherActor = exports.CallWatch
    */
   _onContentFunctionCall: function(...details) {
     // If the consuming tool has finalized call-watcher, ignore the
     // still-instrumented calls.
     if (this._finalized) {
       return;
     }
     let functionCall = new FunctionCallActor(this.conn, details, this._holdWeak);
-    this._functionCalls.push(functionCall);
+
+    if (this._storeCalls) {
+      this._functionCalls.push(functionCall);
+    }
+
     this.onCall(functionCall);
   }
 });
 
 /**
  * The corresponding Front object for the CallWatcherActor.
  */
 let CallWatcherFront = exports.CallWatcherFront = protocol.FrontClass(CallWatcherActor, {
   initialize: function(client, { callWatcherActor }) {
     protocol.Front.prototype.initialize.call(this, client, { actor: callWatcherActor });
-    client.addActorPool(this);
     this.manage(this);
   }
 });
 
 /**
  * Constants.
  */
 CallWatcherFront.METHOD_FUNCTION = 0;
--- a/toolkit/devtools/server/actors/canvas.js
+++ b/toolkit/devtools/server/actors/canvas.js
@@ -251,17 +251,18 @@ let CanvasActor = exports.CanvasActor = 
     }
     this._initialized = true;
 
     this._callWatcher = new CallWatcherActor(this.conn, this.tabActor);
     this._callWatcher.onCall = this._onContentFunctionCall;
     this._callWatcher.setup({
       tracedGlobals: CANVAS_CONTEXTS,
       tracedFunctions: ANIMATION_GENERATORS,
-      performReload: reload
+      performReload: reload,
+      storeCalls: true
     });
   }, {
     request: { reload: Option(0, "boolean") },
     oneway: true
   }),
 
   /**
    * Stops listening for function calls.
@@ -717,17 +718,16 @@ let ContextUtils = {
 };
 
 /**
  * The corresponding Front object for the CanvasActor.
  */
 let CanvasFront = exports.CanvasFront = protocol.FrontClass(CanvasActor, {
   initialize: function(client, { canvasActor }) {
     protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor });
-    client.addActorPool(this);
     this.manage(this);
   }
 });
 
 /**
  * Constants.
  */
 CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS);
--- a/toolkit/devtools/server/actors/device.js
+++ b/toolkit/devtools/server/actors/device.js
@@ -192,17 +192,16 @@ let DeviceActor = protocol.ActorClass({
     };
   }, {request: {},response: { value: RetVal("json")}})
 });
 
 let DeviceFront = protocol.FrontClass(DeviceActor, {
   initialize: function(client, form) {
     protocol.Front.prototype.initialize.call(this, client);
     this.actorID = form.deviceActor;
-    client.addActorPool(this);
     this.manage(this);
   },
 
   screenshotToBlob: function() {
     return this.screenshotToDataURL().then(longstr => {
       return longstr.string().then(dataURL => {
         let deferred = promise.defer();
         longstr.release().then(null, Cu.reportError);
--- a/toolkit/devtools/server/actors/eventlooplag.js
+++ b/toolkit/devtools/server/actors/eventlooplag.js
@@ -79,12 +79,11 @@ let EventLoopLagActor = protocol.ActorCl
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 });
 
 exports.EventLoopLagFront = protocol.FrontClass(EventLoopLagActor, {
   initialize: function(client, form) {
     protocol.Front.prototype.initialize.call(this, client);
     this.actorID = form.eventLoopLagActor;
-    client.addActorPool(this);
     this.manage(this);
   },
 });
--- a/toolkit/devtools/server/actors/gcli.js
+++ b/toolkit/devtools/server/actors/gcli.js
@@ -197,17 +197,16 @@ var GcliActor = protocol.ActorClass({
 
 exports.GcliFront = protocol.FrontClass(GcliActor, {
   initialize: function(client, tabForm) {
     protocol.Front.prototype.initialize.call(this, client);
     this.actorID = tabForm.gcliActor;
 
     // XXX: This is the first actor type in its hierarchy to use the protocol
     // library, so we're going to self-own on the client side for now.
-    client.addActorPool(this);
     this.manage(this);
   },
 });
 
 /**
  * Called the framework on DebuggerServer.registerModule()
  */
 exports.register = function(handle) {
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -2719,17 +2719,16 @@ var InspectorActor = protocol.ActorClass
  */
 var InspectorFront = exports.InspectorFront = protocol.FrontClass(InspectorActor, {
   initialize: function(client, tabForm) {
     protocol.Front.prototype.initialize.call(this, client);
     this.actorID = tabForm.inspectorActor;
 
     // XXX: This is the first actor type in its hierarchy to use the protocol
     // library, so we're going to self-own on the client side for now.
-    client.addActorPool(this);
     this.manage(this);
   },
 
   destroy: function() {
     delete this.walker;
     protocol.Front.prototype.destroy.call(this);
   },
 
--- a/toolkit/devtools/server/actors/layout.js
+++ b/toolkit/devtools/server/actors/layout.js
@@ -128,17 +128,16 @@ let ReflowActor = protocol.ActorClass({
  * let front = ReflowFront(toolbox.target.client, toolbox.target.form);
  * front.on("reflows", this._onReflows);
  * front.start();
  * // now wait for events to come
  */
 exports.ReflowFront = protocol.FrontClass(ReflowActor, {
   initialize: function(client, {reflowActor}) {
     protocol.Front.prototype.initialize.call(this, client, {actor: reflowActor});
-    client.addActorPool(this);
     this.manage(this);
   },
 
   destroy: function() {
     protocol.Front.prototype.destroy.call(this);
   },
 });
 
--- a/toolkit/devtools/server/actors/memory.js
+++ b/toolkit/devtools/server/actors/memory.js
@@ -81,17 +81,16 @@ let MemoryActor = protocol.ActorClass({
 });
 
 exports.MemoryActor = MemoryActor;
 
 exports.MemoryFront = protocol.FrontClass(MemoryActor, {
   initialize: function(client, form) {
     protocol.Front.prototype.initialize.call(this, client, form);
     this.actorID = form.memoryActor;
-    client.addActorPool(this);
     this.manage(this);
   }
 });
 
 exports.register = function(handle) {
   handle.addGlobalActor(MemoryActor, "memoryActor");
   handle.addTabActor(MemoryActor, "memoryActor");
 };
--- a/toolkit/devtools/server/actors/preference.js
+++ b/toolkit/devtools/server/actors/preference.js
@@ -104,17 +104,16 @@ let PreferenceActor = protocol.ActorClas
     response: {}
   }),
 });
 
 let PreferenceFront = protocol.FrontClass(PreferenceActor, {
   initialize: function(client, form) {
     protocol.Front.prototype.initialize.call(this, client);
     this.actorID = form.preferenceActor;
-    client.addActorPool(this);
     this.manage(this);
   },
 });
 
 const _knownPreferenceFronts = new WeakMap();
 
 exports.getPreferenceFront = function(client, form) {
   if (_knownPreferenceFronts.has(client))
--- a/toolkit/devtools/server/actors/storage.js
+++ b/toolkit/devtools/server/actors/storage.js
@@ -1697,13 +1697,11 @@ let StorageActor = exports.StorageActor 
 
 /**
  * Front for the Storage Actor.
  */
 let StorageFront = exports.StorageFront = protocol.FrontClass(StorageActor, {
   initialize: function(client, tabForm) {
     protocol.Front.prototype.initialize.call(this, client);
     this.actorID = tabForm.storageActor;
-
-    client.addActorPool(this);
     this.manage(this);
   }
 });
--- a/toolkit/devtools/server/actors/styleeditor.js
+++ b/toolkit/devtools/server/actors/styleeditor.js
@@ -249,18 +249,16 @@ let StyleEditorActor = protocol.ActorCla
 
 /**
  * The corresponding Front object for the StyleEditorActor.
  */
 let StyleEditorFront = protocol.FrontClass(StyleEditorActor, {
   initialize: function(client, tabForm) {
     protocol.Front.prototype.initialize.call(this, client);
     this.actorID = tabForm.styleEditorActor;
-
-    client.addActorPool(this);
     this.manage(this);
   },
 
   getStyleSheets: function() {
     let deferred = promise.defer();
 
     events.once(this, "document-load", (styleSheets) => {
       deferred.resolve(styleSheets);
--- a/toolkit/devtools/server/actors/stylesheets.js
+++ b/toolkit/devtools/server/actors/stylesheets.js
@@ -264,18 +264,16 @@ let StyleSheetsActor = protocol.ActorCla
 
 /**
  * The corresponding Front object for the StyleSheetsActor.
  */
 let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, {
   initialize: function(client, tabForm) {
     protocol.Front.prototype.initialize.call(this, client);
     this.actorID = tabForm.styleSheetsActor;
-
-    client.addActorPool(this);
     this.manage(this);
   }
 });
 
 /**
  * A MediaRuleActor lives on the server and provides access to properties
  * of a DOM @media rule and emits events when it changes.
  */
--- a/toolkit/devtools/server/actors/webaudio.js
+++ b/toolkit/devtools/server/actors/webaudio.js
@@ -265,17 +265,16 @@ let AudioNodeActor = exports.AudioNodeAc
 });
 
 /**
  * The corresponding Front object for the AudioNodeActor.
  */
 let AudioNodeFront = protocol.FrontClass(AudioNodeActor, {
   initialize: function (client, form) {
     protocol.Front.prototype.initialize.call(this, client, form);
-    client.addActorPool(this);
     this.manage(this);
   }
 });
 
 /**
  * The Web Audio Actor handles simple interaction with an AudioContext
  * high-level methods. After instantiating this actor, you'll need to set it
  * up by calling setup().
@@ -325,17 +324,18 @@ let WebAudioActor = exports.WebAudioActo
     this._initialized = true;
 
     this._callWatcher = new CallWatcherActor(this.conn, this.tabActor);
     this._callWatcher.onCall = this._onContentFunctionCall;
     this._callWatcher.setup({
       tracedGlobals: AUDIO_GLOBALS,
       startRecording: true,
       performReload: reload,
-      holdWeak: true
+      holdWeak: true,
+      storeCalls: false
     });
     // Bind to the `global-destroyed` event on the content observer so we can
     // unbind events between the global destruction and the `finalize` cleanup
     // method on the actor.
     // TODO expose these events on CallWatcherActor itself, bug 1021321
     on(this._callWatcher._contentObserver, "global-destroyed", this._onGlobalDestroyed);
   }, {
     request: { reload: Option(0, "boolean") },
@@ -575,17 +575,16 @@ let WebAudioActor = exports.WebAudioActo
 });
 
 /**
  * The corresponding Front object for the WebAudioActor.
  */
 let WebAudioFront = exports.WebAudioFront = protocol.FrontClass(WebAudioActor, {
   initialize: function(client, { webaudioActor }) {
     protocol.Front.prototype.initialize.call(this, client, { actor: webaudioActor });
-    client.addActorPool(this);
     this.manage(this);
   }
 });
 
 WebAudioFront.NODE_CREATION_METHODS = new Set(NODE_CREATION_METHODS);
 WebAudioFront.NODE_ROUTING_METHODS = new Set(NODE_ROUTING_METHODS);
 
 /**
--- a/toolkit/devtools/server/actors/webgl.js
+++ b/toolkit/devtools/server/actors/webgl.js
@@ -338,17 +338,16 @@ let WebGLActor = exports.WebGLActor = pr
 });
 
 /**
  * The corresponding Front object for the WebGLActor.
  */
 let WebGLFront = exports.WebGLFront = protocol.FrontClass(WebGLActor, {
   initialize: function(client, { webglActor }) {
     protocol.Front.prototype.initialize.call(this, client, { actor: webglActor });
-    client.addActorPool(this);
     this.manage(this);
   }
 });
 
 /**
  * Instruments a HTMLCanvasElement with the appropriate inspection methods.
  */
 let WebGLInstrumenter = {