Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 20 Aug 2013 16:32:33 -0400
changeset 151596 353b662234995befac891e3ed80724a81aab8b3a
parent 151595 cfecdf10dcba63d90663074370c1311b824156ba (current diff)
parent 151279 d136c8999d967cb04004373ba80420217e1a8ce4 (diff)
child 151597 1d29b04fc7c347ed3e41020c65fc69011970e14b
push idunknown
push userunknown
push dateunknown
milestone26.0a1
Merge m-c to inbound.
config/rules.mk
js/src/config/rules.mk
toolkit/devtools/apps/tests/Makefile.in
--- a/b2g/components/UpdatePrompt.js
+++ b/b2g/components/UpdatePrompt.js
@@ -504,50 +504,61 @@ UpdatePrompt.prototype = {
   // nsITimerCallback
 
   notify: function UP_notify(aTimer) {
     if (aTimer == this._applyPromptTimer) {
       log("Timed out waiting for result, restarting");
       this._applyPromptTimer = null;
       this.finishUpdate();
       this._update = null;
+      return;
+    }
+    if (aTimer == this._watchdogTimer) {
+      log("Download watchdog fired");
+      this._watchdogTimer = null;
+      this._autoRestartDownload = true;
+      Services.aus.pauseDownload();
+      return;
     }
   },
 
   createTimer: function UP_createTimer(aTimeoutMs) {
     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     timer.initWithCallback(this, aTimeoutMs, timer.TYPE_ONE_SHOT);
     return timer;
   },
 
   // nsIRequestObserver
 
   _startedSent: false,
 
   _watchdogTimer: null,
-  _watchdogTimeout: 0,
 
   _autoRestartDownload: false,
   _autoRestartCount: 0,
 
-  watchdogTimerFired: function UP_watchdogTimerFired() {
-    log("Download watchdog fired");
-    this._autoRestartDownload = true;
-    Services.aus.pauseDownload();
-  },
-
   startWatchdogTimer: function UP_startWatchdogTimer() {
+    let watchdogTimeout = 120000;  // 120 seconds
+    try {
+      watchdogTimeout = Services.prefs.getIntPref(PREF_DOWNLOAD_WATCHDOG_TIMEOUT);
+    } catch (e) {
+      // This means that the preference doesn't exist. watchdogTimeout will
+      // retain its default assigned above.
+    }
+    if (watchdogTimeout <= 0) {
+      // 0 implies don't bother using the watchdog timer at all.
+      this._watchdogTimer = null;
+      return;
+    }
     if (this._watchdogTimer) {
       this._watchdogTimer.cancel();
     } else {
       this._watchdogTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-      this._watchdogTimeout = Services.prefs.getIntPref(PREF_DOWNLOAD_WATCHDOG_TIMEOUT);
     }
-    this._watchdogTimer.initWithCallback(this.watchdogTimerFired.bind(this),
-                                         this._watchdogTimeout,
+    this._watchdogTimer.initWithCallback(this, watchdogTimeout,
                                          Ci.nsITimer.TYPE_ONE_SHOT);
   },
 
   stopWatchdogTimer: function UP_stopWatchdogTimer() {
     if (this._watchdogTimer) {
       this._watchdogTimer.cancel();
       this._watchdogTimer = null;
     }
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "f98ec5e292dffd144f0a6520cc18ff5cb7762d09", 
+    "revision": "dbaee540a7d23d271e62e7da80d7db226be8860b", 
     "repo_path": "/integration/gaia-central"
 }
--- a/browser/base/content/test/browser_tabopen_reflows.js
+++ b/browser/base/content/test/browser_tabopen_reflows.js
@@ -44,17 +44,21 @@ const EXPECTED_REFLOWS = [
   // SessionStore.getWindowDimensions()
   "ssi_getWindowDimension@resource:///modules/sessionstore/SessionStore.jsm|" +
     "@resource:///modules/sessionstore/SessionStore.jsm|" +
     "ssi_updateWindowFeatures@resource:///modules/sessionstore/SessionStore.jsm|" +
     "ssi_collectWindowData@resource:///modules/sessionstore/SessionStore.jsm|",
 
   // tabPreviews.capture()
   "tabPreviews_capture@chrome://browser/content/browser.js|" +
-    "tabPreviews_handleEvent/<@chrome://browser/content/browser.js|"
+    "tabPreviews_handleEvent/<@chrome://browser/content/browser.js|",
+
+  // tabPreviews.capture()
+  "tabPreviews_capture@chrome://browser/content/browser.js|" +
+    "@chrome://browser/content/browser.js|"
 ];
 
 const PREF_PRELOAD = "browser.newtab.preload";
 
 /*
  * This test ensures that there are no unexpected
  * uninterruptible reflows when opening new tabs.
  */
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -1214,90 +1214,111 @@ ElementEditor.prototype = {
       }
     }
   },
 
   _startModifyingAttributes: function() {
     return this.node.startModifyingAttributes();
   },
 
-  _createAttribute: function EE_createAttribute(aAttr, aBefore)
+  _createAttribute: function EE_createAttribute(aAttr, aBefore = null)
   {
-    if (this.attrs.hasOwnProperty(aAttr.name)) {
-      var attr = this.attrs[aAttr.name];
-      var name = attr.querySelector(".attrname");
-      var val = attr.querySelector(".attrvalue");
-    } else {
-      // Create the template editor, which will save some variables here.
-      let data = {
-        attrName: aAttr.name,
-      };
-      this.template("attribute", data);
-      var {attr, inner, name, val} = data;
+    // Create the template editor, which will save some variables here.
+    let data = {
+      attrName: aAttr.name,
+    };
+    this.template("attribute", data);
+    var {attr, inner, name, val} = data;
 
-      // Figure out where we should place the attribute.
-      let before = aBefore || null;
-      if (aAttr.name == "id") {
-        before = this.attrList.firstChild;
-      } else if (aAttr.name == "class") {
-        let idNode = this.attrs["id"];
-        before = idNode ? idNode.nextSibling : this.attrList.firstChild;
-      }
-      this.attrList.insertBefore(attr, before);
+    // Double quotes need to be handled specially to prevent DOMParser failing.
+    // name="v"a"l"u"e" when editing -> name='v"a"l"u"e"'
+    // name="v'a"l'u"e" when editing -> name="v'a&quot;l'u&quot;e"
+    let editValueDisplayed = aAttr.value;
+    let hasDoubleQuote = editValueDisplayed.contains('"');
+    let hasSingleQuote = editValueDisplayed.contains("'");
+    let initial = aAttr.name + '="' + editValueDisplayed + '"';
+
+    // Can't just wrap value with ' since the value contains both " and '.
+    if (hasDoubleQuote && hasSingleQuote) {
+        editValueDisplayed = editValueDisplayed.replace(/\"/g, "&quot;");
+        initial = aAttr.name + '="' + editValueDisplayed + '"';
+    }
+
+    // Wrap with ' since there are no single quotes in the attribute value.
+    if (hasDoubleQuote && !hasSingleQuote) {
+        initial = aAttr.name + "='" + editValueDisplayed + "'";
+    }
 
-      // Make the attribute editable.
-      editableField({
-        element: inner,
-        trigger: "dblclick",
-        stopOnReturn: true,
-        selectAll: false,
-        contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
-        popup: this.markup.popup,
-        start: (aEditor, aEvent) => {
-          // If the editing was started inside the name or value areas,
-          // select accordingly.
-          if (aEvent && aEvent.target === name) {
-            aEditor.input.setSelectionRange(0, name.textContent.length);
-          } else if (aEvent && aEvent.target === val) {
-            let length = val.textContent.length;
-            let editorLength = aEditor.input.value.length;
-            let start = editorLength - (length + 1);
-            aEditor.input.setSelectionRange(start, start + length);
-          } else {
-            aEditor.input.select();
-          }
-        },
-        done: (aVal, aCommit) => {
-          if (!aCommit) {
-            return;
-          }
+    // Make the attribute editable.
+    editableField({
+      element: inner,
+      trigger: "dblclick",
+      stopOnReturn: true,
+      selectAll: false,
+      initial: initial,
+      contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+      popup: this.markup.popup,
+      start: (aEditor, aEvent) => {
+        // If the editing was started inside the name or value areas,
+        // select accordingly.
+        if (aEvent && aEvent.target === name) {
+          aEditor.input.setSelectionRange(0, name.textContent.length);
+        } else if (aEvent && aEvent.target === val) {
+          let length = editValueDisplayed.length;
+          let editorLength = aEditor.input.value.length;
+          let start = editorLength - (length + 1);
+          aEditor.input.setSelectionRange(start, start + length);
+        } else {
+          aEditor.input.select();
+        }
+      },
+      done: (aVal, aCommit) => {
+        if (!aCommit) {
+          return;
+        }
+
+        let doMods = this._startModifyingAttributes();
+        let undoMods = this._startModifyingAttributes();
 
-          let doMods = this._startModifyingAttributes();
-          let undoMods = this._startModifyingAttributes();
+        // Remove the attribute stored in this editor and re-add any attributes
+        // parsed out of the input element. Restore original attribute if
+        // parsing fails.
+        try {
+          this._saveAttribute(aAttr.name, undoMods);
+          doMods.removeAttribute(aAttr.name);
+          this._applyAttributes(aVal, attr, doMods, undoMods);
+          this.undo.do(() => {
+            doMods.apply();
+          }, () => {
+            undoMods.apply();
+          })
+        } catch(ex) {
+          console.error(ex);
+        }
+      }
+    });
 
-          // Remove the attribute stored in this editor and re-add any attributes
-          // parsed out of the input element. Restore original attribute if
-          // parsing fails.
-          try {
-            this._saveAttribute(aAttr.name, undoMods);
-            doMods.removeAttribute(aAttr.name);
-            this._applyAttributes(aVal, attr, doMods, undoMods);
-            this.undo.do(() => {
-              doMods.apply();
-            }, () => {
-              undoMods.apply();
-            })
-          } catch(ex) {
-            console.error(ex);
-          }
-        }
-      });
 
-      this.attrs[aAttr.name] = attr;
+    // Figure out where we should place the attribute.
+    let before = aBefore;
+    if (aAttr.name == "id") {
+      before = this.attrList.firstChild;
+    } else if (aAttr.name == "class") {
+      let idNode = this.attrs["id"];
+      before = idNode ? idNode.nextSibling : this.attrList.firstChild;
     }
+    this.attrList.insertBefore(attr, before);
+
+    // Remove the old version of this attribute from the DOM.
+    let oldAttr = this.attrs[aAttr.name];
+    if (oldAttr && oldAttr.parentNode) {
+      oldAttr.parentNode.removeChild(oldAttr);
+    }
+
+    this.attrs[aAttr.name] = attr;
 
     name.textContent = aAttr.name;
     val.textContent = aAttr.value;
 
     return attr;
   },
 
   /**
--- a/browser/devtools/markupview/test/browser_inspector_markup_edit.html
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.html
@@ -35,10 +35,12 @@
     </div>
     <div id="node22" class="unchanged"></div>
     <div id="node23"></div>
     <div id="node24"></div>
     <div id="retag-me">
       <div id="retag-me-2"></div>
     </div>
     <div id="node25"></div>
+    <div id="node26" style='background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F");'></div>
+    <div id="node27" class="Double &quot; and single &apos;"></div>
   </body>
 </html>
--- a/browser/devtools/markupview/test/browser_inspector_markup_edit.js
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.js
@@ -234,16 +234,89 @@ function test() {
       },
       after: function() {
         assertAttributes(doc.querySelector("#node25"), {
           id: "node25",
           src: "somefile.html?param1=<a>&param2=\xfc&param3='\"'"
         });
       }
     },
+
+    {
+      desc: "Modify inline style containing \"",
+      before: function() {
+        assertAttributes(doc.querySelector("#node26"), {
+          id: "node26",
+          style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F");'
+        });
+      },
+      execute: function(after) {
+        inspector.once("markupmutation", after);
+        let editor = getContainerForRawNode(markup, doc.querySelector("#node26")).editor;
+        let attr = editor.attrs["style"].querySelector(".editable");
+
+
+        attr.focus();
+        EventUtils.sendKey("return", inspector.panelWin);
+
+        let input = inplaceEditor(attr).input;
+        let value = input.value;
+
+        is (value,
+          "style='background-image: url(\"moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F\");'",
+          "Value contains actual double quotes"
+        );
+
+        value = value.replace(/mozilla\.org/, "mozilla.com");
+        input.value = value;
+
+        EventUtils.sendKey("return", inspector.panelWin);
+      },
+      after: function() {
+        assertAttributes(doc.querySelector("#node26"), {
+          id: "node26",
+          style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.com%2F");'
+        });
+      }
+    },
+
+    {
+      desc: "Modify inline style containing \" and \'",
+      before: function() {
+        assertAttributes(doc.querySelector("#node27"), {
+          id: "node27",
+          class: 'Double " and single \''
+        });
+      },
+      execute: function(after) {
+        inspector.once("markupmutation", after);
+        let editor = getContainerForRawNode(markup, doc.querySelector("#node27")).editor;
+        let attr = editor.attrs["class"].querySelector(".editable");
+
+        attr.focus();
+        EventUtils.sendKey("return", inspector.panelWin);
+
+        let input = inplaceEditor(attr).input;
+        let value = input.value;
+
+        is (value, "class=\"Double &quot; and single '\"", "Value contains &quot;");
+
+        value = value.replace(/Double/, "&quot;").replace(/single/, "'");
+        input.value = value;
+
+        EventUtils.sendKey("return", inspector.panelWin);
+      },
+      after: function() {
+        assertAttributes(doc.querySelector("#node27"), {
+          id: "node27",
+          class: '" " and \' \''
+        });
+      }
+    },
+
     {
       desc: "Add an attribute value without closing \"",
       enteredText: 'style="display: block;',
       expectedAttributes: {
         style: "display: block;"
       }
     },
     {
--- a/browser/devtools/responsivedesign/responsivedesign.jsm
+++ b/browser/devtools/responsivedesign/responsivedesign.jsm
@@ -682,16 +682,17 @@ ResponsiveUI.prototype = {
 
     this.saveCustomSize();
 
     delete this._resizing;
     if (this.transitionsEnabled) {
       this.stack.removeAttribute("notransition");
     }
     this.ignoreY = false;
+    this.ignoreX = false;
     this.isResizing = false;
   },
 
   /**
    * Store the custom size as a pref.
    */
    saveCustomSize: function RUI_saveCustomSize() {
      Services.prefs.setIntPref("devtools.responsiveUI.customWidth", this.customPreset.width);
--- a/browser/devtools/styleinspector/test/browser_ruleview_inherit.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_inherit.js
@@ -37,17 +37,17 @@ function simpleInherit(aInspector, aRule
     ok(!!inheritRule.inherited, "Rule should consider itself inherited.");
     is(inheritRule.textProps.length, 1, "Should only display one inherited style");
     let inheritProp = inheritRule.textProps[0];
     is(inheritProp.name, "color", "color should have been inherited.");
 
     styleNode.parentNode.removeChild(styleNode);
 
     emptyInherit();
-  }).then(null, console.error);
+  });
 }
 
 function emptyInherit()
 {
   // No inheritable styles, this rule shouldn't show up.
   let style = '' +
     '#test2 {' +
     '  background-color: green;' +
@@ -63,17 +63,17 @@ function emptyInherit()
     is(elementStyle.rules.length, 1, "Should have 1 rule.");
 
     let elementRule = elementStyle.rules[0];
     ok(!elementRule.inherited, "Element style attribute should not consider itself inherited.");
 
     styleNode.parentNode.removeChild(styleNode);
 
     elementStyleInherit();
-  }).then(null, console.error);
+  });
 }
 
 function elementStyleInherit()
 {
   doc.body.innerHTML = '<div id="test2" style="color: red"><div id="test1">Styled Node</div></div>';
 
   inspector.selection.setNode(doc.getElementById("test1"));
   inspector.once("inspector-updated", () => {
@@ -87,17 +87,17 @@ function elementStyleInherit()
     let inheritRule = elementStyle.rules[1];
     is(inheritRule.domRule.type, ELEMENT_STYLE, "Inherited rule should be an element style, not a rule.");
     ok(!!inheritRule.inherited, "Rule should consider itself inherited.");
     is(inheritRule.textProps.length, 1, "Should only display one inherited style");
     let inheritProp = inheritRule.textProps[0];
     is(inheritProp.name, "color", "color should have been inherited.");
 
     finishTest();
-  }).then(null, console.error);
+  });
 }
 
 function finishTest()
 {
   doc = null;
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js
@@ -32,17 +32,17 @@ function SI_inspectNode()
   var span = doc.querySelector("#matches");
   ok(span, "captain, we have the matches span");
 
   inspector.selection.setNode(span);
   inspector.once("inspector-updated", () => {
     is(span, computedView.viewedElement.rawNode(),
       "style inspector node matches the selected node");
     SI_toggleDefaultStyles();
-  }).then(null, (err) => console.error(err));
+  });
 }
 
 function SI_toggleDefaultStyles()
 {
   info("checking \"Browser styles\" checkbox");
 
   let doc = computedView.styleDocument;
   let checkbox = doc.querySelector(".includebrowserstyles");
--- a/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
@@ -55,17 +55,17 @@
 <!ENTITY btnPageCSS.label   "CSS">
 <!ENTITY btnPageCSS.tooltip "Log CSS parsing errors">
 <!ENTITY btnPageCSS.accesskey "C">
 <!ENTITY btnPageJS.label    "JS">
 <!ENTITY btnPageJS.tooltip  "Log JavaScript exceptions">
 <!ENTITY btnPageJS.accesskey  "J">
 <!ENTITY btnPageSecurity.label "Security">
 <!ENTITY btnPageSecurity.tooltip "Log security errors and warnings">
-<!ENTITY btnPageSecurity.accesskey "S">
+<!ENTITY btnPageSecurity.accesskey "u">
 
 <!-- LOCALIZATION NOTE (btnPageLogging): This is used as the text of the
   -  the toolbar. It shows or hides messages that the web developer inserted on
   -  the page for debugging purposes, using calls such console.log() and
   -  console.error(). -->
 <!ENTITY btnPageLogging.label   "Logging">
 <!ENTITY btnPageLogging.tooltip "Log messages sent to the window.console object">
 <!ENTITY btnPageLogging.accesskey2 "R">
--- a/browser/themes/shared/devtools/markup-view.css
+++ b/browser/themes/shared/devtools/markup-view.css
@@ -4,22 +4,16 @@
 
 * {
   padding: 0;
   margin: 0;
 }
 
 .newattr {
   cursor: pointer;
-}
-
-/* Give some padding to focusable elements to match the editor input
- * that will replace them. */
-span[tabindex] {
-  display: inline-block;
   padding: 1px 0;
 }
 
 li.container {
   padding: 2px 0 0 2px;
 }
 
 .codebox {
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -25,16 +25,17 @@ endif
   HOST_CSRCS \
   HOST_LIBRARY_NAME \
   MODULE \
   NO_DIST_INSTALL \
   PARALLEL_DIRS \
   TEST_DIRS \
   TIERS \
   TOOL_DIRS \
+  XPCSHELL_TESTS \
   XPIDL_MODULE \
   $(NULL)
 
 _DEPRECATED_VARIABLES := \
   XPIDL_FLAGS \
   $(NULL)
 
 ifndef EXTERNALLY_MANAGED_MAKE_FILE
--- a/dom/indexedDB/ipc/IndexedDBParent.cpp
+++ b/dom/indexedDB/ipc/IndexedDBParent.cpp
@@ -77,17 +77,19 @@ IndexedDBParent::IndexedDBParent(TabPare
 IndexedDBParent::~IndexedDBParent()
 {
   MOZ_COUNT_DTOR(IndexedDBParent);
 }
 
 void
 IndexedDBParent::Disconnect()
 {
-  MOZ_ASSERT(!mDisconnected);
+  if (mDisconnected) {
+    return;
+  }
 
   mDisconnected = true;
 
   const InfallibleTArray<PIndexedDBDatabaseParent*>& databases =
     ManagedPIndexedDBDatabaseParent();
   for (uint32_t i = 0; i < databases.Length(); ++i) {
     static_cast<IndexedDBDatabaseParent*>(databases[i])->Disconnect();
   }
--- a/dom/locales/en-US/chrome/layout/HtmlForm.properties
+++ b/dom/locales/en-US/chrome/layout/HtmlForm.properties
@@ -23,13 +23,16 @@ NoFileSelected=No file selected.
 # LOCALIZATION NOTE (NoFilesSelected): this string is shown on a
 # <input type='file' multiple> when there is no file selected yet.
 NoFilesSelected=No files selected.
 # LOCALIZATION NOTE (XFilesSelected): this string is shown on a
 # <input type='file' multiple> when there are more than one selected file.
 # %S will be a number greater or equal to 2.
 XFilesSelected=%S files selected.
 ColorPicker=Choose a color
-# LOCALIZATION NOTE (AndXMoreFiles): this string is shown at the end of the
-# tooltip text for <input type='file' multiple> when there are more than 21
-# files selected (when we will only list the first 20, plus an "and X more"
-# line). %S will be the number of files minus 20. 
-AndXMoreFiles=and %S more
+# LOCALIZATION NOTE (AndNMoreFiles): Semi-colon list of plural forms. 
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals 
+# This string is shown at the end of the tooltip text for <input type='file'
+# multiple> when there are more than 21 files selected (when we will only list
+# the first 20, plus an "and X more" line). #1 represents the number of files
+# minus 20 and will always be a number equal to or greater than 2. So the
+# singular case will never be used.
+AndNMoreFiles=and one more;and #1 more
--- a/dom/mobilemessage/src/gonk/MmsService.js
+++ b/dom/mobilemessage/src/gonk/MmsService.js
@@ -2013,19 +2013,29 @@ MmsService.prototype = {
       };
       // Update the delivery status to pending in DB.
       gMobileMessageDatabaseService
         .setMessageDeliveryByMessageId(aMessageId,
                                        null,
                                        null,
                                        DELIVERY_STATUS_PENDING,
                                        null,
-                                       this.retrieveMessage(url,
-                                                            responseNotify.bind(this),
-                                                            aDomMessage));
+                                       (function (rv) {
+          let success = Components.isSuccessCode(rv);
+          if (!success) {
+            if (DEBUG) debug("Could not change the delivery status: MMS " +
+                             domMessage.id + ", error code " + rv);
+            aRequest.notifyGetMessageFailed(Ci.nsIMobileMessageCallback.INTERNAL_ERROR);
+            return;
+          }
+
+          this.retrieveMessage(url,
+                               responseNotify.bind(this),
+                               aDomMessage);
+        }).bind(this));
     }).bind(this));
   },
 
   // nsIWapPushApplication
 
   receiveWapPush: function receiveWapPush(array, length, offset, options) {
     let data = {array: array, offset: offset};
     let msg = MMS.PduHelper.parse(data, null);
--- a/dom/system/gonk/RadioInterfaceLayer.js
+++ b/dom/system/gonk/RadioInterfaceLayer.js
@@ -3227,16 +3227,18 @@ RadioInterface.prototype = {
     this.workerMessenger.send("deactivateDataCall", { cid: cid,
                                                       reason: reason });
   },
 };
 
 function RILNetworkInterface(radioInterface, apnSetting) {
   this.radioInterface = radioInterface;
   this.apnSetting = apnSetting;
+
+  this.connectedTypes = [];
 }
 
 RILNetworkInterface.prototype = {
   classID:   RILNETWORKINTERFACE_CID,
   classInfo: XPCOMUtils.generateCI({classID: RILNETWORKINTERFACE_CID,
                                     classDescription: "RILNetworkInterface",
                                     interfaces: [Ci.nsINetworkInterface,
                                                  Ci.nsIRILDataCallback]}),
@@ -3397,22 +3399,22 @@ RILNetworkInterface.prototype = {
   },
 
   // Helpers
 
   cid: null,
   registeredAsDataCallCallback: false,
   registeredAsNetworkInterface: false,
   connecting: false,
-  apnSetting: {},
+  apnSetting: null,
 
   // APN failed connections. Retry counter
   apnRetryCounter: 0,
 
-  connectedTypes: [],
+  connectedTypes: null,
 
   inConnectedTypes: function inConnectedTypes(type) {
     return this.connectedTypes.indexOf(type) != -1;
   },
 
   get connected() {
     return this.state == RIL.GECKO_NETWORK_STATE_CONNECTED;
   },
--- a/dom/wifi/WifiWorker.js
+++ b/dom/wifi/WifiWorker.js
@@ -159,17 +159,21 @@ var WifiManager = (function() {
   function voidControlMessage(cmd, callback) {
     controlMessage({ cmd: cmd }, function (data) {
       callback(data.status);
     });
   }
 
   var driverLoaded = false;
 
-  manager.getDriverLoaded = function() { return driverLoaded; }
+  manager.checkDriverState = function(expectState) {
+    if (!unloadDriverEnabled)
+      return true;
+    return (expectState === driverLoaded);
+  }
 
   function loadDriver(callback) {
     if (driverLoaded) {
       callback(0);
       return;
     }
 
     voidControlMessage("load_driver", function(status) {
@@ -2838,59 +2842,59 @@ WifiWorker.prototype = {
   },
 
   _notifyAfterStateChange: function(success, newState) {
     if (!this._stateRequests.length)
       return;
 
     // First, notify all of the requests that were trying to make this change.
     let state = this._stateRequests[0].enabled;
-    let driverLoaded = WifiManager.getDriverLoaded();
+    let driverReady = WifiManager.checkDriverState(newState);
 
     // It is callback function's responsibility to handle the pending request.
     // So we just return here.
     if (this._stateRequests.length > 0
         && ("callback" in this._stateRequests[0])) {
       return;
     }
 
     // If the new state is not the same as state or new state is not the same as
     // driver loaded state, then we weren't processing the first request (we
     // were racing somehow) so don't notify.
     // For newState is false(disable), we expect driverLoaded is false(driver unloaded)
     // to proceed, and vice versa.
-    if (!success || (newState === driverLoaded && state === newState)) {
+    if (!success || (driverReady && state === newState)) {
       do {
         if (!("callback" in this._stateRequests[0])) {
           this._stateRequests.shift();
         }
         // Don't remove more than one request if the previous one failed.
       } while (success &&
                this._stateRequests.length &&
                !("callback" in this._stateRequests[0]) &&
                this._stateRequests[0].enabled === state);
     }
     // If there were requests queued after this one, run them.
     if (this._stateRequests.length > 0) {
       let self = this;
       let callback = null;
       this._callbackTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-      if (newState === driverLoaded) {
-        // Driver status is as same as new state, proceed next request.
+      if (driverReady) {
+        // Driver is ready for next request.
         callback = function(timer) {
           if ("callback" in self._stateRequests[0]) {
             self._stateRequests[0].callback.call(self, self._stateRequests[0].enabled);
           } else {
             WifiManager.setWifiEnabled(self._stateRequests[0].enabled,
                                        self._setWifiEnabledCallback.bind(self));
           }
           timer = null;
         };
       } else {
-        // Driver status is not as same as new state, wait driver.
+        // Wait driver until it's ready.
         callback = function(timer) {
           self._notifyAfterStateChange(success, newState);
           timer = null;
         };
       }
       this._callbackTimer.initWithCallback(callback, 1000, Ci.nsITimer.TYPE_ONE_SHOT);
     }
   },
--- a/js/src/config/rules.mk
+++ b/js/src/config/rules.mk
@@ -25,16 +25,17 @@ endif
   HOST_CSRCS \
   HOST_LIBRARY_NAME \
   MODULE \
   NO_DIST_INSTALL \
   PARALLEL_DIRS \
   TEST_DIRS \
   TIERS \
   TOOL_DIRS \
+  XPCSHELL_TESTS \
   XPIDL_MODULE \
   $(NULL)
 
 _DEPRECATED_VARIABLES := \
   XPIDL_FLAGS \
   $(NULL)
 
 ifndef EXTERNALLY_MANAGED_MAKE_FILE
--- a/mobile/android/base/PageActionLayout.java
+++ b/mobile/android/base/PageActionLayout.java
@@ -26,39 +26,39 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
 import java.util.UUID;
-import java.util.LinkedHashMap;
+import java.util.ArrayList;
 
 public class PageActionLayout extends LinearLayout implements GeckoEventListener,
                                                               View.OnClickListener,
                                                               View.OnLongClickListener {
     private final String LOGTAG = "GeckoPageActionLayout";
     private final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY";
     private final int DEFAULT_PAGE_ACTIONS_SHOWN = 2;
 
-    private LinkedHashMap<String, PageAction> mPageActionList;
+    private ArrayList<PageAction> mPageActionList;
     private GeckoPopupMenu mPageActionsMenu;
     private Context mContext;
     private LinearLayout mLayout;
 
     // By default it's two, can be changed by calling setNumberShown(int)
     private int mMaxVisiblePageActions;
 
     public PageActionLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
         mLayout = this;
 
-        mPageActionList = new LinkedHashMap<String, PageAction>();
+        mPageActionList = new ArrayList<PageAction>();
         setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN);
         refreshPageActionIcons();
 
         registerEventListener("PageActions:Add");
         registerEventListener("PageActions:Remove");
     }
 
     public void setNumberShown(int count) {
@@ -86,57 +86,68 @@ public class PageActionLayout extends Li
 
     @Override
     public void handleMessage(String event, JSONObject message) {
         try {
             if (event.equals("PageActions:Add")) {
                 final String id = message.getString("id");
                 final String title = message.getString("title");
                 final String imageURL = message.optString("icon");
+                final boolean mImportant = message.getBoolean("important");
 
                 addPageAction(id, title, imageURL, new OnPageActionClickListeners() {
                     @Override
                     public void onClick(String id) {
                         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:Clicked", id));
                     }
 
                     @Override
                     public boolean onLongClick(String id) {
                         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:LongClicked", id));
                         return true;
                     }
-                });
+                }, mImportant);
             } else if (event.equals("PageActions:Remove")) {
                 final String id = message.getString("id");
 
                 removePageAction(id);
             }
         } catch(JSONException ex) {
             Log.e(LOGTAG, "Error deocding", ex);
         }
     }
 
-    public void addPageAction(final String id, final String title, final String imageData, final OnPageActionClickListeners mOnPageActionClickListeners) {
-        final PageAction pageAction = new PageAction(id, title, null, mOnPageActionClickListeners);
-        mPageActionList.put(id, pageAction);
+    public void addPageAction(final String id, final String title, final String imageData, final OnPageActionClickListeners mOnPageActionClickListeners, boolean mImportant) {
+        final PageAction pageAction = new PageAction(id, title, null, mOnPageActionClickListeners, mImportant);
+
+        int insertAt = mPageActionList.size();
+        while(insertAt > 0 && mPageActionList.get(insertAt-1).isImportant()) {
+          insertAt--;
+        }
+        mPageActionList.add(insertAt, pageAction);
 
         BitmapUtils.getDrawable(mContext, imageData, new BitmapUtils.BitmapLoader() {
             @Override
             public void onBitmapFound(final Drawable d) {
-                if (mPageActionList.containsKey(id)) {
+                if (mPageActionList.contains(pageAction)) {
                     pageAction.setDrawable(d);
                     refreshPageActionIcons();
                 }
             }
         });
     }
 
     public void removePageAction(String id) {
-        mPageActionList.remove(id);
-        refreshPageActionIcons();
+        for(int i = 0; i < mPageActionList.size(); i++) {
+            if (mPageActionList.get(i).getID().equals(id)) {
+                mPageActionList.remove(i);
+                refreshPageActionIcons();
+                return;
+            }
+        }
     }
 
     private ImageButton createImageButton() {
         ImageButton imageButton = new ImageButton(mContext, null, R.style.AddressBar_ImageButton_Icon);
         imageButton.setLayoutParams(new LayoutParams(mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width), LayoutParams.MATCH_PARENT));
         imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
         imageButton.setOnClickListener(this);
         imageButton.setOnLongClickListener(this);
@@ -145,29 +156,29 @@ public class PageActionLayout extends Li
 
     @Override
     public void onClick(View v) {
         String buttonClickedId = (String)v.getTag();
         if (buttonClickedId != null) {
             if (buttonClickedId.equals(MENU_BUTTON_KEY)) {
                 showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1);
             } else {
-                mPageActionList.get(buttonClickedId).onClick();
+                getPageActionWithId(buttonClickedId).onClick();
             }
         }
     }
 
     @Override
     public boolean onLongClick(View v) {
         String buttonClickedId = (String)v.getTag();
         if (buttonClickedId.equals(MENU_BUTTON_KEY)) {
             showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1);
             return true;
         } else {
-            return mPageActionList.get(buttonClickedId).onLongClick();
+            return getPageActionWithId(buttonClickedId).onLongClick();
         }
     }
 
     private void setActionForView(final ImageButton view, final PageAction pageAction) {
         if (pageAction == null) {
             view.setTag(null);
             ThreadUtils.postToUiThread(new Runnable() {
                 @Override
@@ -225,79 +236,86 @@ public class PageActionLayout extends Li
          * and hence we maintain the insertion order of the child Views which is essentially the reverse of their index
          */
 
         int buttonIndex = (this.getChildCount() - 1) - index;
         int totalVisibleButtons = ((mPageActionList.size() < this.getChildCount()) ? mPageActionList.size() : this.getChildCount());
 
         if (mPageActionList.size() > buttonIndex) {
             // Return the pageactions starting from the end of the list for the number of visible pageactions.
-            return getPageActionAt((mPageActionList.size() - totalVisibleButtons) + buttonIndex);
+            return mPageActionList.get((mPageActionList.size() - totalVisibleButtons) + buttonIndex);
         }
         return null;
     }
 
-    private PageAction getPageActionAt(int index) {
-        int count = 0;
-        for(PageAction pageAction : mPageActionList.values()) {
-            if (count == index) {
+    private PageAction getPageActionWithId(String id) {
+        for(int i = 0; i < mPageActionList.size(); i++) {
+            PageAction pageAction = mPageActionList.get(i);
+            if (pageAction.getID().equals(id)) {
                 return pageAction;
             }
-            count++;
         }
         return null;
     }
 
     private void showMenu(View mPageActionButton, int toShow) {
         if (mPageActionsMenu == null) {
             mPageActionsMenu = new GeckoPopupMenu(mPageActionButton.getContext(), mPageActionButton);
             mPageActionsMenu.inflate(0);
             mPageActionsMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() {
                 @Override
                 public boolean onMenuItemClick(MenuItem item) {
-                    for(PageAction pageAction : mPageActionList.values()) {
-                        if (pageAction.key() == item.getItemId()) {
+                    int id = item.getItemId();
+                    for(int i = 0; i < mPageActionList.size(); i++) {
+                        PageAction pageAction = mPageActionList.get(i);
+                        if (pageAction.key() == id) {
                             pageAction.onClick();
+                            return true;
                         }
                     }
-                    return true;
+                    return false;
                 }
             });
         }
         Menu menu = mPageActionsMenu.getMenu();
         menu.clear();
 
-        int count = 0;
-        for(PageAction pageAction : mPageActionList.values()) {
-            if (count < toShow) {
+        for(int i = 0; i < mPageActionList.size(); i++) {
+            if (i < toShow) {
+                PageAction pageAction = mPageActionList.get(i);
                 MenuItem item = menu.add(Menu.NONE, pageAction.key(), Menu.NONE, pageAction.getTitle());
                 item.setIcon(pageAction.getDrawable());
             }
-            count++;
         }
         mPageActionsMenu.show();
     }
 
     public static interface OnPageActionClickListeners {
         public void onClick(String id);
         public boolean onLongClick(String id);
     }
 
     private static class PageAction {
         private OnPageActionClickListeners mOnPageActionClickListeners;
         private Drawable mDrawable;
         private String mTitle;
         private String mId;
         private int key;
+        private boolean mImportant;
 
-        public PageAction(String id, String title, Drawable image, OnPageActionClickListeners mOnPageActionClickListeners) {
+        public PageAction(String id,
+                          String title,
+                          Drawable image,
+                          OnPageActionClickListeners mOnPageActionClickListeners,
+                          boolean mImportant) {
             this.mId = id;
             this.mTitle = title;
             this.mDrawable = image;
             this.mOnPageActionClickListeners = mOnPageActionClickListeners;
+            this.mImportant = mImportant;
 
             this.key = UUID.fromString(mId.subSequence(1, mId.length() - 2).toString()).hashCode();
         }
 
         public Drawable getDrawable() {
             return mDrawable;
         }
 
@@ -312,16 +330,20 @@ public class PageActionLayout extends Li
         public String getID() {
             return mId;
         }
 
         public int key() {
             return key;
         }
 
+        public boolean isImportant() {
+            return mImportant;
+        }
+
         public void onClick() {
             if (mOnPageActionClickListeners != null) {
                 mOnPageActionClickListeners.onClick(mId);
             }
         }
 
         public boolean onLongClick() {
             if (mOnPageActionClickListeners != null) {
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -69,23 +69,27 @@ var SelectionHandler = {
     switch (aTopic) {
       case "Gesture:SingleTap": {
         if (this._activeType == this.TYPE_SELECTION) {
           let data = JSON.parse(aData);
           if (this._pointInSelection(data.x, data.y))
             this.copySelection();
           else
             this._closeSelection();
+        } else if (this._activeType == this.TYPE_CURSOR) {
+          // attachCaret() is called in the "Gesture:SingleTap" handler in BrowserEventHandler
+          // We're guaranteed to call this first, because this observer was added last
+          this._closeSelection();
         }
         break;
       }
       case "Tab:Selected":
         this._closeSelection();
         break;
-  
+
       case "Window:Resize": {
         if (this._activeType == this.TYPE_SELECTION) {
           // Knowing when the page is done drawing is hard, so let's just cancel
           // the selection when the window changes. We should fix this later.
           this._closeSelection();
         }
         break;
       }
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1623,17 +1623,18 @@ var NativeWindow = {
   pageactions: {
     _items: { },
     add: function(aOptions) {
       let id = uuidgen.generateUUID().toString();
       sendMessageToJava({
         type: "PageActions:Add",
         id: id,
         title: aOptions.title,
-        icon: resolveGeckoURI(aOptions.icon)
+        icon: resolveGeckoURI(aOptions.icon),
+        important: "important" in aOptions ? aOptions.important : false
       });
       this._items[id] = {
         clickCallback: aOptions.clickCallback,
         longClickCallback: aOptions.longClickCallback
       };
       return id;
     },
     remove: function(id) {
@@ -7152,24 +7153,26 @@ let Reader = {
       NativeWindow.pageactions.remove(this.pageAction.id);
       delete this.pageAction.id;
     }
 
     if (tab.readerActive) {
       this.pageAction.id = NativeWindow.pageactions.add({
         title: Strings.browser.GetStringFromName("readerMode.exit"),
         icon: "drawable://reader_active",
-        clickCallback: this.pageAction.readerModeCallback
+        clickCallback: this.pageAction.readerModeCallback,
+        important: true
       });
     } else if (tab.readerEnabled) {
       this.pageAction.id = NativeWindow.pageactions.add({
         title: Strings.browser.GetStringFromName("readerMode.enter"),
         icon: "drawable://reader",
         clickCallback:this.pageAction.readerModeCallback,
-        longClickCallback: this.pageAction.readerModeActiveCallback
+        longClickCallback: this.pageAction.readerModeActiveCallback,
+        important: true
       });
     }
   },
 
   observe: function(aMessage, aTopic, aData) {
     switch(aTopic) {
       case "Reader:Add": {
         let args = JSON.parse(aData);
--- a/services/common/tests/unit/test_storage_server.js
+++ b/services/common/tests/unit/test_storage_server.js
@@ -341,17 +341,17 @@ add_test(function test_bso_if_unmodified
   server.createContents("123", {
     test: {bso: {foo: "bar"}}
   });
   server.startSynchronous();
 
   let coll = server.user("123").collection("test");
   do_check_neq(coll, null);
 
-  let time = coll.timestamp;
+  let time = coll.bso("bso").modified;
 
   _("Ensure we get a 412 for specified times older than server time.");
   let request = localRequest(server, "/2.0/123/storage/test/bso",
                              "123", "password");
   request.setHeader("X-If-Unmodified-Since", time - 5000);
   request.setHeader("Content-Type", "application/json");
   let payload = JSON.stringify({"payload": "foobar"});
   let error = doPutRequest(request, payload);
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -3361,16 +3361,28 @@
     "description": "The time (in milliseconds) that it took a 'prototypeAndProperties' request to go round trip."
   },
   "DEVTOOLS_DEBUGGER_RDP_REMOTE_PROTOTYPEANDPROPERTIES_MS": {
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "1000",
     "description": "The time (in milliseconds) that it took a 'prototypeAndProperties' request to go round trip."
   },
+  "DEVTOOLS_DEBUGGER_RDP_LOCAL_PROTOTYPESANDPROPERTIES_MS": {
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'prototypesAndProperties' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_REMOTE_PROTOTYPESANDPROPERTIES_MS": {
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'prototypesAndProperties' request to go round trip."
+  },
   "DEVTOOLS_DEBUGGER_RDP_LOCAL_PROPERTY_MS": {
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "1000",
     "description": "The time (in milliseconds) that it took a 'property' request to go round trip."
   },
   "DEVTOOLS_DEBUGGER_RDP_REMOTE_PROPERTY_MS": {
     "kind": "exponential",
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -585,21 +585,21 @@
                 const TRUNCATED_FILE_COUNT = 20;
                 let count = Math.min(files.length, TRUNCATED_FILE_COUNT);
                 for (let i = 1; i < count; ++i) {
                   titleText += "\n" + files[i].name;
                 }
                 if (files.length == TRUNCATED_FILE_COUNT + 1) {
                   titleText += "\n" + files[TRUNCATED_FILE_COUNT].name;
                 } else if (files.length > TRUNCATED_FILE_COUNT + 1) {
-                  let xmoreStr = bundle.GetStringFromName("AndXMoreFiles");
+                  let xmoreStr = bundle.GetStringFromName("AndNMoreFiles");
                   let xmoreNum = files.length - TRUNCATED_FILE_COUNT;
                   let tmp = {};
                   Components.utils.import("resource://gre/modules/PluralForm.jsm", tmp);
-                  let andXMoreStr = tmp.PluralForm.get(xmoreNum, xmoreStr).replace("%S", xmoreNum);
+                  let andXMoreStr = tmp.PluralForm.get(xmoreNum, xmoreStr).replace("#1", xmoreNum);
                   titleText += "\n" + andXMoreStr;
                 }
               }
             } catch(e) {}
           }
 
           while ((titleText == null) && (XLinkTitleText == null) &&
                  (SVGTitleText == null) && tipElement) {
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -1,35 +1,46 @@
 /* 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";
 
 /* General utilities used throughout devtools. */
 
-/* Turn the error e into a string, without fail. */
+/**
+ * Turn the error |aError| into a string, without fail.
+ */
 this.safeErrorString = function safeErrorString(aError) {
   try {
-    var s = aError.toString();
-    if (typeof s === "string")
-      return s;
+    let errorString = aError.toString();
+    if (typeof errorString === "string") {
+      // Attempt to attach a stack to |errorString|. If it throws an error, or
+      // isn't a string, don't use it.
+      try {
+        if (aError.stack) {
+          let stack = aError.stack.toString();
+          if (typeof stack === "string") {
+            errorString += "\nStack: " + stack;
+          }
+        }
+      } catch (ee) { }
+
+      return errorString;
+    }
   } catch (ee) { }
 
   return "<failed trying to find error description>";
 }
 
 /**
  * Report that |aWho| threw an exception, |aException|.
  */
 this.reportException = function reportException(aWho, aException) {
   let msg = aWho + " threw an exception: " + safeErrorString(aException);
-  if (aException.stack) {
-    msg += "\nCall stack:\n" + aException.stack;
-  }
 
   dump(msg + "\n");
 
   if (Components.utils.reportError) {
     /*
      * Note that the xpcshell test harness registers an observer for
      * console messages, so when we're running tests, this will cause
      * the test to quit.
deleted file mode 100644
--- a/toolkit/devtools/apps/tests/Makefile.in
+++ /dev/null
@@ -1,15 +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/.
-
-DEPTH           = @DEPTH@
-topsrcdir       = @top_srcdir@
-srcdir          = @srcdir@
-VPATH           = @srcdir@
-relativesrcdir = @relativesrcdir@
-
-include $(DEPTH)/config/autoconf.mk
-
-XPCSHELL_TESTS = unit
-
-include $(topsrcdir)/config/rules.mk
--- a/toolkit/devtools/apps/tests/moz.build
+++ b/toolkit/devtools/apps/tests/moz.build
@@ -1,7 +1,9 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 MODULE = 'test_webapps_actor'
+
+XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
--- a/toolkit/devtools/apps/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/apps/tests/unit/xpcshell.ini
@@ -1,6 +1,10 @@
 [DEFAULT]
 head = head_apps.js
 tail = tail_apps.js
 
 [test_webappsActor.js]
+# Persistent failures.
+skip-if = true
 [test_appInstall.js]
+# Persistent failures.
+skip-if = true
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -1621,18 +1621,32 @@ ThreadClient.prototype = {
    */
   source: function TC_source(aForm) {
     if (aForm.actor in this._threadGrips) {
       return this._threadGrips[aForm.actor];
     }
 
     return this._threadGrips[aForm.actor] = new SourceClient(this._client,
                                                              aForm);
-  }
+  },
 
+  /**
+   * Request the prototype and own properties of mutlipleObjects.
+   *
+   * @param aOnResponse function
+   *        Called with the request's response.
+   * @param actors [string]
+   *        List of actor ID of the queried objects.
+   */
+  getPrototypesAndProperties: DebuggerClient.requester({
+    type: "prototypesAndProperties",
+    actors: args(0)
+  }, {
+    telemetry: "PROTOTYPESANDPROPERTIES"
+  })
 };
 
 eventSource(ThreadClient.prototype);
 
 /**
  * Creates a tracing profiler client for the remote debugging protocol
  * server. This client is a front to the trace actor created on the
  * server side, hiding the protocol details in a traditional
--- a/toolkit/devtools/moz.build
+++ b/toolkit/devtools/moz.build
@@ -1,14 +1,16 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+TEST_DIRS += ['tests']
+
 PARALLEL_DIRS += [
     'server',
     'client',
     'gcli',
     'sourcemap',
     'webconsole',
     'apps',
     'styleinspector'
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -2065,31 +2065,60 @@ ThreadActor.prototype = {
           && bp.line <= endLine) {
         this._setBreakpoint(bp);
       }
     }
 
     return true;
   },
 
+
+  /**
+   * Get prototypes and properties of multiple objects.
+   */
+  onPrototypesAndProperties: function TA_onPrototypesAndProperties(aRequest) {
+    let result = {};
+    for (let actorID of aRequest.actors) {
+      // This code assumes that there are no lazily loaded actors returned
+      // by this call.
+      let actor = this.conn.getActor(actorID);
+      if (!actor) {
+        return { from: this.actorID,
+                 error: "noSuchActor" };
+      }
+      let handler = actor.onPrototypeAndProperties;
+      if (!handler) {
+        return { from: this.actorID,
+                 error: "unrecognizedPacketType",
+                 message: ('Actor "' + actorID +
+                           '" does not recognize the packet type ' +
+                           '"prototypeAndProperties"') };
+      }
+      result[actorID] = handler.call(actor, {});
+    }
+    return { from: this.actorID,
+             actors: result };
+  }
+
 };
 
 ThreadActor.prototype.requestTypes = {
   "attach": ThreadActor.prototype.onAttach,
   "detach": ThreadActor.prototype.onDetach,
   "reconfigure": ThreadActor.prototype.onReconfigure,
   "resume": ThreadActor.prototype.onResume,
   "clientEvaluate": ThreadActor.prototype.onClientEvaluate,
   "frames": ThreadActor.prototype.onFrames,
   "interrupt": ThreadActor.prototype.onInterrupt,
   "eventListeners": ThreadActor.prototype.onEventListeners,
   "releaseMany": ThreadActor.prototype.onReleaseMany,
   "setBreakpoint": ThreadActor.prototype.onSetBreakpoint,
   "sources": ThreadActor.prototype.onSources,
-  "threadGrips": ThreadActor.prototype.onThreadGrips
+  "threadGrips": ThreadActor.prototype.onThreadGrips,
+  "prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties
 };
 
 
 /**
  * Creates a PauseActor.
  *
  * PauseActors exist for the lifetime of a given debuggee pause.  Used to
  * scope pause-lifetime grips.
--- a/toolkit/devtools/server/actors/tracer.js
+++ b/toolkit/devtools/server/actors/tracer.js
@@ -530,17 +530,17 @@ function timeSinceTraceStarted({ startTi
 /**
  * Creates a value grip for the given completion value, to be
  * serialized by JSON.stringify.
  *
  * @param aType string
  *        The type of completion value to serialize (return, throw, or yield).
  */
 function serializeCompletionValue(aType, { value }) {
-  if (typeof value[aType] === "undefined") {
+  if (!Object.hasOwnProperty.call(value, aType)) {
     return undefined;
   }
   return createValueGrip(value[aType], true);
 }
 
 
 // Serialization helper functions. Largely copied from script.js and modified
 // for use in serialization rather than object actor requests.
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -829,23 +829,22 @@ DebuggerServerConnection.prototype = {
       if (pool.has(aActorID)) {
         return pool;
       }
     }
     return null;
   },
 
   _unknownError: function DSC__unknownError(aPrefix, aError) {
-    let errorString = safeErrorString(aError);
-    errorString += "\n" + aError.stack;
+    let errorString = aPrefix + ": " + safeErrorString(aError);
     Cu.reportError(errorString);
     dumpn(errorString);
     return {
       error: "unknownError",
-      message: (aPrefix + "': " + errorString)
+      message: errorString
     };
   },
 
   /* Forwarding packets to other transports based on actor name prefixes. */
 
   /*
    * Arrange to forward packets to another server. This is how we
    * forward debugging connections to child processes.
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-09.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/**
+ * This tests exercises getProtypesAndProperties message accepted
+ * by a thread actor.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-grips");
+  gDebuggee.eval(function stopMe(arg1, arg2) {
+    debugger;
+  }.toString());
+
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_object_grip();
+    });
+  });
+  do_test_pending();
+}
+
+function test_object_grip()
+{
+  gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
+    let args = aPacket.frame.arguments;
+
+    gThreadClient.getPrototypesAndProperties([args[0].actor, args[1].actor], function(aResponse) {
+      let obj1 = aResponse.actors[args[0].actor];
+      let obj2 = aResponse.actors[args[1].actor];
+      do_check_eq(obj1.ownProperties.x.configurable, true);
+      do_check_eq(obj1.ownProperties.x.enumerable, true);
+      do_check_eq(obj1.ownProperties.x.writable, true);
+      do_check_eq(obj1.ownProperties.x.value, 10);
+
+      do_check_eq(obj1.ownProperties.y.configurable, true);
+      do_check_eq(obj1.ownProperties.y.enumerable, true);
+      do_check_eq(obj1.ownProperties.y.writable, true);
+      do_check_eq(obj1.ownProperties.y.value, "kaiju");
+
+      do_check_eq(obj2.ownProperties.z.configurable, true);
+      do_check_eq(obj2.ownProperties.z.enumerable, true);
+      do_check_eq(obj2.ownProperties.z.writable, true);
+      do_check_eq(obj2.ownProperties.z.value, 123);
+
+      do_check_true(obj1.prototype != undefined);
+      do_check_true(obj2.prototype != undefined);
+
+      gThreadClient.resume(function() {
+        finishClient(gClient);
+      });
+    });
+
+  });
+
+  gDebuggee.eval("stopMe({ x: 10, y: 'kaiju'}, { z: 123 })");
+}
+
--- a/toolkit/devtools/server/tests/unit/test_trace_actor-06.js
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-06.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Tests that objects are correctly serialized and sent in exitedFrame
- * packets.
+ * Tests that values are correctly serialized and sent in enteredFrame
+ * and exitedFrame packets.
  */
 
 let {defer} = devtools.require("sdk/core/promise");
 
 var gDebuggee;
 var gClient;
 var gTraceClient;
 
@@ -25,122 +25,199 @@ function run_test()
       });
     });
   });
   do_test_pending();
 }
 
 function test_enter_exit_frame()
 {
-  gTraceClient.addListener("exitedFrame", function(aEvent, aPacket) {
-    if (aPacket.sequence === 3) {
-      let obj = aPacket.return;
-      do_check_eq(typeof obj, "object",
-                  'exitedFrame response should have return value');
-      do_check_eq(typeof obj.prototype, "object",
-                  'return value should have prototype');
-      do_check_eq(typeof obj.ownProperties, "object",
-                  'return value should have ownProperties list');
-      do_check_eq(typeof obj.safeGetterValues, "object",
-                  'return value should have safeGetterValues');
-
-      do_check_eq(typeof obj.ownProperties.num, "object",
-                  'return value should have property "num"');
-      do_check_eq(typeof obj.ownProperties.str, "object",
-                  'return value should have property "str"');
-      do_check_eq(typeof obj.ownProperties.bool, "object",
-                  'return value should have property "bool"');
-      do_check_eq(typeof obj.ownProperties.undef, "object",
-                  'return value should have property "undef"');
-      do_check_eq(typeof obj.ownProperties.undef.value, "object",
-                  'return value property "undef" should be a grip');
-      do_check_eq(typeof obj.ownProperties.nil, "object",
-                  'return value should have property "nil"');
-      do_check_eq(typeof obj.ownProperties.nil.value, "object",
-                  'return value property "nil" should be a grip');
-      do_check_eq(typeof obj.ownProperties.obj, "object",
-                  'return value should have property "obj"');
-      do_check_eq(typeof obj.ownProperties.obj.value, "object",
-                  'return value property "obj" should be a grip');
-      do_check_eq(typeof obj.ownProperties.arr, "object",
-                  'return value should have property "arr"');
-      do_check_eq(typeof obj.ownProperties.arr.value, "object",
-                  'return value property "arr" should be a grip');
-      do_check_eq(typeof obj.ownProperties.inf, "object",
-                  'return value should have property "inf"');
-      do_check_eq(typeof obj.ownProperties.inf.value, "object",
-                  'return value property "inf" should be a grip');
-      do_check_eq(typeof obj.ownProperties.ninf, "object",
-                  'return value should have property "ninf"');
-      do_check_eq(typeof obj.ownProperties.ninf.value, "object",
-                  'return value property "ninf" should be a grip');
-      do_check_eq(typeof obj.ownProperties.nan, "object",
-                  'return value should have property "nan"');
-      do_check_eq(typeof obj.ownProperties.nan.value, "object",
-                  'return value property "nan" should be a grip');
-      do_check_eq(typeof obj.ownProperties.nzero, "object",
-                  'return value should have property "nzero"');
-      do_check_eq(typeof obj.ownProperties.nzero.value, "object",
-                  'return value property "nzero" should be a grip');
-
-      do_check_eq(obj.prototype.type, "object");
-      do_check_eq(obj.ownProperties.num.value, 25);
-      do_check_eq(obj.ownProperties.str.value, "foo");
-      do_check_eq(obj.ownProperties.bool.value, false);
-      do_check_eq(obj.ownProperties.undef.value.type, "undefined");
-      do_check_eq(obj.ownProperties.nil.value.type, "null");
-      do_check_eq(obj.ownProperties.obj.value.type, "object");
-      do_check_eq(obj.ownProperties.obj.value.class, "Object");
-      do_check_eq(obj.ownProperties.arr.value.type, "object");
-      do_check_eq(obj.ownProperties.arr.value.class, "Array");
-      do_check_eq(obj.ownProperties.inf.value.type, "Infinity");
-      do_check_eq(obj.ownProperties.ninf.value.type, "-Infinity");
-      do_check_eq(obj.ownProperties.nan.value.type, "NaN");
-      do_check_eq(obj.ownProperties.nzero.value.type, "-0");
-    }
-  });
+  gTraceClient.addListener("enteredFrame", check_packet);
+  gTraceClient.addListener("exitedFrame", check_packet);
 
   start_trace()
     .then(eval_code)
     .then(stop_trace)
     .then(function() {
       finishClient(gClient);
     });
 }
 
 function start_trace()
 {
   let deferred = defer();
-  gTraceClient.startTrace(["return"], null, function() { deferred.resolve(); });
+  gTraceClient.startTrace(["arguments", "return"], null, function() { deferred.resolve(); });
   return deferred.promise;
 }
 
 function eval_code()
 {
   gDebuggee.eval("(" + function() {
-    function foo() {
-      let obj = {};
-      obj.self = obj;
+    function identity(x) {
+      return x;
+    }
+
+    let circular = {};
+    circular.self = circular;
 
-      return {
-        num: 25,
-        str: "foo",
-        bool: false,
-        undef: undefined,
-        nil: null,
-        obj: obj,
-        arr: [1,2,3,4,5],
-        inf: Infinity,
-        ninf: -Infinity,
-        nan: NaN,
-        nzero: -0
-      };
-    }
-    foo();
+    let obj = {
+      num: 0,
+      str: "foo",
+      bool: false,
+      undef: undefined,
+      nil: null,
+      inf: Infinity,
+      ninf: -Infinity,
+      nan: NaN,
+      nzero: -0,
+      obj: circular,
+      arr: [1,2,3,4,5]
+    };
+
+    identity();
+    identity(0);
+    identity("");
+    identity(false);
+    identity(undefined);
+    identity(null);
+    identity(Infinity);
+    identity(-Infinity);
+    identity(NaN);
+    identity(-0);
+    identity(obj);
   } + ")()");
 }
 
 function stop_trace()
 {
   let deferred = defer();
   gTraceClient.stopTrace(null, function() { deferred.resolve(); });
   return deferred.promise;
 }
+
+function check_packet(aEvent, aPacket)
+{
+  let value = (aPacket.type === "enteredFrame" && aPacket.arguments)
+        ? aPacket.arguments[0]
+        : aPacket.return;
+  switch(aPacket.sequence) {
+  case 2:
+    do_check_eq(typeof aPacket.arguments, "object",
+                "zero-argument function call should send arguments list");
+    do_check_eq(aPacket.arguments.length, 0,
+                "zero-argument function call should send zero-length arguments list");
+    break;
+  case 3:
+    check_value(value, "object", "undefined");
+    break;
+  case 4:
+  case 5:
+    check_value(value, "number", 0);
+    break;
+  case 6:
+  case 7:
+    check_value(value, "string", "");
+    break;
+  case 8:
+  case 9:
+    check_value(value, "boolean", false);
+    break;
+  case 10:
+  case 11:
+    check_value(value, "object", "undefined");
+    break;
+  case 12:
+  case 13:
+    check_value(value, "object", "null");
+    break;
+  case 14:
+  case 15:
+    check_value(value, "object", "Infinity");
+    break;
+  case 16:
+  case 17:
+    check_value(value, "object", "-Infinity");
+    break;
+  case 18:
+  case 19:
+    check_value(value, "object", "NaN");
+    break;
+  case 20:
+  case 21:
+    check_value(value, "object", "-0");
+    break;
+  case 22:
+  case 23:
+    check_object(aPacket.type, value);
+    break;
+  }
+}
+
+function check_value(aActual, aExpectedType, aExpectedValue)
+{
+  do_check_eq(typeof aActual, aExpectedType);
+  do_check_eq(aExpectedType === "object" ? aActual.type : aActual, aExpectedValue);
+}
+
+function check_object(aType, aObj) {
+  do_check_eq(typeof aObj, "object",
+              'serialized object should be present in packet');
+  do_check_eq(typeof aObj.prototype, "object",
+              'serialized object should have prototype');
+  do_check_eq(typeof aObj.ownProperties, "object",
+              'serialized object should have ownProperties list');
+  do_check_eq(typeof aObj.safeGetterValues, "object",
+              'serialized object should have safeGetterValues');
+
+  do_check_eq(typeof aObj.ownProperties.num, "object",
+              'serialized object should have property "num"');
+  do_check_eq(typeof aObj.ownProperties.str, "object",
+              'serialized object should have property "str"');
+  do_check_eq(typeof aObj.ownProperties.bool, "object",
+              'serialized object should have property "bool"');
+  do_check_eq(typeof aObj.ownProperties.undef, "object",
+              'serialized object should have property "undef"');
+  do_check_eq(typeof aObj.ownProperties.undef.value, "object",
+              'serialized object property "undef" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.nil, "object",
+              'serialized object should have property "nil"');
+  do_check_eq(typeof aObj.ownProperties.nil.value, "object",
+              'serialized object property "nil" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.obj, "object",
+              'serialized object should have property "aObj"');
+  do_check_eq(typeof aObj.ownProperties.obj.value, "object",
+              'serialized object property "aObj" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.arr, "object",
+              'serialized object should have property "arr"');
+  do_check_eq(typeof aObj.ownProperties.arr.value, "object",
+              'serialized object property "arr" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.inf, "object",
+              'serialized object should have property "inf"');
+  do_check_eq(typeof aObj.ownProperties.inf.value, "object",
+              'serialized object property "inf" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.ninf, "object",
+              'serialized object should have property "ninf"');
+  do_check_eq(typeof aObj.ownProperties.ninf.value, "object",
+              'serialized object property "ninf" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.nan, "object",
+              'serialized object should have property "nan"');
+  do_check_eq(typeof aObj.ownProperties.nan.value, "object",
+              'serialized object property "nan" should be a grip');
+  do_check_eq(typeof aObj.ownProperties.nzero, "object",
+              'serialized object should have property "nzero"');
+  do_check_eq(typeof aObj.ownProperties.nzero.value, "object",
+              'serialized object property "nzero" should be a grip');
+
+  do_check_eq(aObj.prototype.type, "object");
+  do_check_eq(aObj.ownProperties.num.value, 0);
+  do_check_eq(aObj.ownProperties.str.value, "foo");
+  do_check_eq(aObj.ownProperties.bool.value, false);
+  do_check_eq(aObj.ownProperties.undef.value.type, "undefined");
+  do_check_eq(aObj.ownProperties.nil.value.type, "null");
+  do_check_eq(aObj.ownProperties.obj.value.type, "object");
+  do_check_eq(aObj.ownProperties.obj.value.class, "Object");
+  do_check_eq(aObj.ownProperties.arr.value.type, "object");
+  do_check_eq(aObj.ownProperties.arr.value.class, "Array");
+  do_check_eq(aObj.ownProperties.inf.value.type, "Infinity");
+  do_check_eq(aObj.ownProperties.ninf.value.type, "-Infinity");
+  do_check_eq(aObj.ownProperties.nan.value.type, "NaN");
+  do_check_eq(aObj.ownProperties.nzero.value.type, "-0");
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -122,16 +122,17 @@ reason = bug 820380
 [test_objectgrips-01.js]
 [test_objectgrips-02.js]
 [test_objectgrips-03.js]
 [test_objectgrips-04.js]
 [test_objectgrips-05.js]
 [test_objectgrips-06.js]
 [test_objectgrips-07.js]
 [test_objectgrips-08.js]
+[test_objectgrips-09.js]
 [test_interrupt.js]
 [test_stepping-01.js]
 [test_stepping-02.js]
 [test_stepping-03.js]
 [test_stepping-04.js]
 [test_stepping-05.js]
 [test_stepping-06.js]
 [test_framebindings-01.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MODULE = 'test_devtools'
+
+XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/unit/head_devtools.js
@@ -0,0 +1,42 @@
+"use strict";
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+let errorCount = 0;
+let listener = {
+  observe: function (aMessage) {
+    errorCount++;
+    try {
+      // If we've been given an nsIScriptError, then we can print out
+      // something nicely formatted, for tools like Emacs to pick up.
+      var scriptError = aMessage.QueryInterface(Ci.nsIScriptError);
+      dump(aMessage.sourceName + ":" + aMessage.lineNumber + ": " +
+           scriptErrorFlagsToKind(aMessage.flags) + ": " +
+           aMessage.errorMessage + "\n");
+      var string = aMessage.errorMessage;
+    } catch (x) {
+      // Be a little paranoid with message, as the whole goal here is to lose
+      // no information.
+      try {
+        var string = "" + aMessage.message;
+      } catch (x) {
+        var string = "<error converting error message to string>";
+      }
+    }
+
+    // Make sure we exit all nested event loops so that the test can finish.
+    while (DebuggerServer.xpcInspector.eventLoopNestLevel > 0) {
+      DebuggerServer.xpcInspector.exitNestedEventLoop();
+    }
+    do_throw("head_dbg.js got console message: " + string + "\n");
+  }
+};
+
+let consoleService = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);
+consoleService.registerListener(listener);
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/unit/test_safeErrorString.js
@@ -0,0 +1,54 @@
+/* -*- Mode: js; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test DevToolsUtils.safeErrorString
+
+function run_test() {
+  test_with_error();
+  test_with_tricky_error();
+  test_with_string();
+  test_with_thrower();
+  test_with_psychotic();
+}
+
+function test_with_error() {
+  let s = DevToolsUtils.safeErrorString(new Error("foo bar"));
+  // Got the message.
+  do_check_true(s.contains("foo bar"));
+  // Got the stack.
+  do_check_true(s.contains("test_with_error"))
+  do_check_true(s.contains("test_safeErrorString.js"));
+}
+
+function test_with_tricky_error() {
+  let e = new Error("batman");
+  e.stack = { toString: Object.create(null) };
+  let s = DevToolsUtils.safeErrorString(e);
+  // Still got the message, despite a bad stack property.
+  do_check_true(s.contains("batman"));
+}
+
+function test_with_string() {
+  let s = DevToolsUtils.safeErrorString("not really an error");
+  // Still get the message.
+  do_check_true(s.contains("not really an error"));
+}
+
+function test_with_thrower() {
+  let s = DevToolsUtils.safeErrorString({
+    toString: () => {
+      throw new Error("Muahahaha");
+    }
+  });
+  // Still don't fail, get string back.
+  do_check_eq(typeof s, "string");
+}
+
+function test_with_psychotic() {
+  let s = DevToolsUtils.safeErrorString({
+    toString: () => Object.create(null)
+  });
+  // Still get a string out, and no exceptions thrown
+  do_check_eq(typeof s, "string");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/unit/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head = head_devtools.js
+tail =
+
+[test_safeErrorString.js]
\ No newline at end of file
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -2202,16 +2202,21 @@ NS_IMETHODIMP nsExternalAppHandler::Canc
 {
   NS_ENSURE_ARG(NS_FAILED(aReason));
   // XXX should not ignore the reason
 
   mCanceled = true;
   if (mSaver) {
     mSaver->Finish(aReason);
     mSaver = nullptr;
+  } else if (mStopRequestIssued && mTempFile) {
+    // This branch can only happen when the user cancels the helper app dialog
+    // when the request has completed. The temp file has to be removed here,
+    // because mSaver has been released at that time with the temp file left.
+    (void)mTempFile->Remove(false);
   }
 
   // Break our reference cycle with the helper app dialog (set up in
   // OnStartRequest)
   mDialog = nullptr;
 
   mRequest = nullptr;
 
--- a/webapprt/ContentPermission.js
+++ b/webapprt/ContentPermission.js
@@ -1,84 +1,105 @@
 /* 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/. */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
+const UNKNOWN_FAIL = ["geolocation", "desktop-notification"];
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://webapprt/modules/WebappRT.jsm");
 
 function ContentPermission() {}
 
 ContentPermission.prototype = {
   classID: Components.ID("{07ef5b2e-88fb-47bd-8cec-d3b0bef11ac4}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
 
+  _getChromeWindow: function(aWindow) { 
+    return aWindow
+      .QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIWebNavigation)
+      .QueryInterface(Ci.nsIDocShellTreeItem)
+      .rootTreeItem
+      .QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIDOMWindow)
+      .QueryInterface(Ci.nsIDOMChromeWindow);
+  },
+
   prompt: function(request) {
-    // Only handle geolocation requests for now
-    if (request.type != "geolocation") {
-      return;
+    // Reuse any remembered permission preferences
+    let result =
+      Services.perms.testExactPermissionFromPrincipal(request.principal,
+                                                      request.type);
+
+    // We used to use the name "geo" for the geolocation permission, now we're
+    // using "geolocation".  We need to check both to support existing
+    // installations.
+    if ((result == Ci.nsIPermissionManager.UNKNOWN_ACTION ||
+         result == Ci.nsIPermissionManager.PROMPT_ACTION) &&
+        request.type == "geolocation") {
+      let geoResult = Services.perms.testExactPermission(request.principal.URI,
+                                                         "geo");
+      // We override the result only if the "geo" permission was allowed or
+      // denied.
+      if (geoResult == Ci.nsIPermissionManager.ALLOW_ACTION ||
+          geoResult == Ci.nsIPermissionManager.DENY_ACTION) {
+        result = geoResult;
+      }
     }
 
-    // Reuse any remembered permission preferences
-    let result = Services.perms.testExactPermissionFromPrincipal(request.principal, "geo");
     if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
       request.allow();
       return;
-    }
-    else if (result == Ci.nsIPermissionManager.DENY_ACTION) {
+    } else if (result == Ci.nsIPermissionManager.DENY_ACTION ||
+               (result == Ci.nsIPermissionManager.UNKNOWN_ACTION &&
+                UNKNOWN_FAIL.indexOf(request.type) >= 0)) {
       request.cancel();
       return;
     }
 
-    function getChromeWindow(aWindow) {
-      var chromeWin = aWindow
-        .QueryInterface(Ci.nsIInterfaceRequestor)
-        .getInterface(Ci.nsIWebNavigation)
-        .QueryInterface(Ci.nsIDocShellTreeItem)
-        .rootTreeItem
-        .QueryInterface(Ci.nsIInterfaceRequestor)
-        .getInterface(Ci.nsIDOMWindow)
-        .QueryInterface(Ci.nsIDOMChromeWindow);
-      return chromeWin;
-    }
-
     // Display a prompt at the top level
     let {name} = WebappRT.config.app.manifest;
     let requestingWindow = request.window.top;
-    let chromeWin = getChromeWindow(requestingWindow);
+    let chromeWin = this._getChromeWindow(requestingWindow);
     let bundle = Services.strings.createBundle("chrome://webapprt/locale/webapp.properties");
 
     // Construct a prompt with share/don't and remember checkbox
     let remember = {value: false};
     let choice = Services.prompt.confirmEx(
       chromeWin,
-      bundle.formatStringFromName("geolocation.title", [name], 1),
-      bundle.GetStringFromName("geolocation.description"),
+      bundle.formatStringFromName(request.type + ".title", [name], 1),
+      bundle.GetStringFromName(request.type + ".description"),
       // Set both buttons to strings with the cancel button being default
       Ci.nsIPromptService.BUTTON_POS_1_DEFAULT |
         Ci.nsIPromptService.BUTTON_TITLE_IS_STRING * Ci.nsIPromptService.BUTTON_POS_0 |
         Ci.nsIPromptService.BUTTON_TITLE_IS_STRING * Ci.nsIPromptService.BUTTON_POS_1,
-      bundle.GetStringFromName("geolocation.sharelocation"),
-      bundle.GetStringFromName("geolocation.dontshare"),
+      bundle.GetStringFromName(request.type + ".allow"),
+      bundle.GetStringFromName(request.type + ".deny"),
       null,
-      bundle.GetStringFromName("geolocation.remember"),
+      bundle.GetStringFromName(request.type + ".remember"),
       remember);
 
-    // Persist the choice if the user wants to remember
+    let action = Ci.nsIPermissionManager.ALLOW_ACTION;
+    if (choice != 0) {
+      action = Ci.nsIPermissionManager.DENY_ACTION;
+    }
+
     if (remember.value) {
-      let action = Ci.nsIPermissionManager.ALLOW_ACTION;
-      if (choice != 0) {
-        action = Ci.nsIPermissionManager.DENY_ACTION;
-      }
-      Services.perms.addFromPrincipal(request.principal, "geo", action);
+      // Persist the choice if the user wants to remember
+      Services.perms.addFromPrincipal(request.principal, request.type, action);
+    } else {
+      // Otherwise allow the permission for the current session
+      Services.perms.addFromPrincipal(request.principal, request.type, action,
+                                      Ci.nsIPermissionManager.EXPIRE_SESSION);
     }
 
     // Trigger the selected choice
     if (choice == 0) {
       request.allow();
     }
     else {
       request.cancel();
--- a/webapprt/content/webapp.xul
+++ b/webapprt/content/webapp.xul
@@ -40,17 +40,17 @@
        key="&copyCmd.key;"
        modifiers="accel"/>
   <key id="key_paste"
        key="&pasteCmd.key;"
        modifiers="accel"/>
   <key id="key_delete" keycode="VK_DELETE" command="cmd_delete"/>
   <key id="key_selectAll" key="&selectAllCmd.key;" modifiers="accel"/>
   <key id="key_quitApplication"
-       key="&quitApplicationCmdMac.key;"
+       key="&quitApplicationCmdUnix.key;"
        command="cmd_quitApplication"
        modifiers="accel"/>
   <key id="key_hideThisAppCmdMac"
        key="&hideThisAppCmdMac.key;"
        modifiers="accel"/>
   <key id="key_hideOtherAppsCmdMac"
        key="&hideOtherAppsCmdMac.key;"
        modifiers="accel,alt"/>
--- a/webapprt/locales/en-US/webapprt/webapp.dtd
+++ b/webapprt/locales/en-US/webapprt/webapp.dtd
@@ -13,22 +13,24 @@
 
 <!ENTITY quitApplicationCmdWin.label        "Exit">
 <!ENTITY quitApplicationCmdWin.accesskey    "x">
 <!ENTITY quitApplicationCmd.label           "Quit">
 <!ENTITY quitApplicationCmd.accesskey       "Q">
 <!-- On Mac, we create the Quit and Hide command labels dynamically,
    - using properties in window.properties, in order to include the name
    - of the webapp in the labels without creating a DTD file for it. -->
-<!ENTITY quitApplicationCmdMac.key          "Q">
 <!ENTITY hideThisAppCmdMac.key              "H">
 <!ENTITY hideOtherAppsCmdMac.label          "Hide Others">
 <!ENTITY hideOtherAppsCmdMac.key            "H">
 <!ENTITY showAllAppsCmdMac.label            "Show All">
 
+<!-- LOCALIZATION NOTE(quitApplicationCmdUnix.key): This keyboard shortcut is used by both Linux and OSX -->
+<!ENTITY quitApplicationCmdUnix.key          "Q">
+
 <!ENTITY editMenu.label                     "Edit">
 <!ENTITY editMenu.accesskey                 "E">
 <!ENTITY undoCmd.label                      "Undo">
 <!ENTITY undoCmd.key                        "Z">
 <!ENTITY undoCmd.accesskey                  "U">
 <!ENTITY redoCmd.label                      "Redo">
 <!ENTITY redoCmd.key                        "Y">
 <!ENTITY redoCmd.accesskey                  "R">
--- a/webapprt/locales/en-US/webapprt/webapp.properties
+++ b/webapprt/locales/en-US/webapprt/webapp.properties
@@ -15,20 +15,28 @@ quitApplicationCmdMac.label=Quit %S
 # LOCALIZATION NOTE (hideApplicationCmdMac.label): %S will be replaced with
 # the name of the webapp.
 hideApplicationCmdMac.label=Hide %S
 
 # LOCALIZATION NOTE (geolocation.title): %S will be replaced with the name of
 # the webapp.
 geolocation.title=%S - Share Location
 geolocation.description=Do you want to share your location?
-geolocation.sharelocation=Share Location
-geolocation.dontshare=Don't Share
+geolocation.allow=Share Location
+geolocation.deny=Don't Share
 geolocation.remember=Remember my choice
 
+# LOCALIZATION NOTE (desktop-notification.title): %S will be replaced with the
+# name of the webapp.
+desktop-notification.title=%S - Show notifications
+desktop-notification.description=Do you want to allow notifications?
+desktop-notification.allow=Show
+desktop-notification.deny=Don't show
+desktop-notification.remember=Remember my choice
+
 # LOCALIZATION NOTE (webapps.install.title): %S will be replaced with the name
 # of the webapp being installed.
 webapps.install.title=Install %S
 # LOCALIZATION NOTE (webapps.install.description): %S will be replaced with the
 # name of the webapp being installed.
 webapps.install.description=Do you want to install %S?
 webapps.install.install=Install App
 webapps.install.dontinstall=Don't Install