Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 04 Apr 2016 23:38:50 -0400
changeset 347729 e02a9b1b4931191f2fccc397e75ae8c28ef47460
parent 347728 4ebc3275aa3d5b9d7dc222864b5a29a24582c9eb (current diff)
parent 347519 fd37367d1b1f2952c7a970c4b41e650d7233c717 (diff)
child 347730 954444d01cd10f263850121c87c706ca65682cd3
push id14653
push userolivier@olivieryiptong.com
push dateTue, 05 Apr 2016 19:21:01 +0000
reviewersmerge
milestone48.0a1
Merge m-c to inbound. a=merge
--- a/browser/config/mozconfigs/common
+++ b/browser/config/mozconfigs/common
@@ -1,7 +1,10 @@
 # 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/.
 
 # This file is included by all browser mozconfigs
 
 . "$topsrcdir/build/mozconfig.common"
+
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
--- a/browser/config/mozconfigs/linux32/common-opt
+++ b/browser/config/mozconfigs/linux32/common-opt
@@ -6,15 +6,13 @@ ac_add_options --with-google-api-keyfile
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-desktop-geoloc-api.key
 
 . $topsrcdir/build/unix/mozconfig.linux32
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
-export MOZ_TELEMETRY_REPORTING=1
-
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
--- a/browser/config/mozconfigs/linux64/common-opt
+++ b/browser/config/mozconfigs/linux64/common-opt
@@ -6,15 +6,13 @@ ac_add_options --with-google-api-keyfile
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-desktop-geoloc-api.key
 
 . $topsrcdir/build/unix/mozconfig.linux
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
-export MOZ_TELEMETRY_REPORTING=1
-
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
--- a/browser/config/mozconfigs/macosx-universal/common-opt
+++ b/browser/config/mozconfigs/macosx-universal/common-opt
@@ -9,15 +9,13 @@ ac_add_options --enable-update-channel=$
 ac_add_options --enable-update-packaging
 ac_add_options --with-google-api-keyfile=/builds/gapi.data
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-desktop-geoloc-api.key
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
-export MOZ_TELEMETRY_REPORTING=1
-
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
--- a/browser/config/mozconfigs/win32/common-opt
+++ b/browser/config/mozconfigs/win32/common-opt
@@ -20,18 +20,16 @@ else
   _google_oauth_api_keyfile=/e/builds/google-oauth-api.key
 fi
 ac_add_options --with-google-oauth-api-keyfile=${_google_oauth_api_keyfile}
 ac_add_options --with-mozilla-api-keyfile=/c/builds/mozilla-desktop-geoloc-api.key
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
-export MOZ_TELEMETRY_REPORTING=1
-
 . $topsrcdir/build/win32/mozconfig.vs2015-win64
 
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 # Enable Adobe Primetime CDM on 32-bit Windows in Mozilla builds.
 # Enabled here on the assumption that downstream vendors will not be using
 # these build configs.
--- a/browser/config/mozconfigs/win64/common-opt
+++ b/browser/config/mozconfigs/win64/common-opt
@@ -18,18 +18,16 @@ else
   _google_oauth_api_keyfile=/e/builds/google-oauth-api.key
 fi
 ac_add_options --with-google-oauth-api-keyfile=${_google_oauth_api_keyfile}
 ac_add_options --with-mozilla-api-keyfile=/c/builds/mozilla-desktop-geoloc-api.key
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
-export MOZ_TELEMETRY_REPORTING=1
-
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 . $topsrcdir/build/win64/mozconfig.vs2015
 
 # Enable Adobe Primetime CDM on 64-bit Windows in Mozilla builds.
 # Enabled here on the assumption that downstream vendors will not be using
 # these build configs.
--- a/devtools/client/locales/en-US/storage.properties
+++ b/devtools/client/locales/en-US/storage.properties
@@ -110,8 +110,12 @@ storage.search.placeholder=Filter values
 
 # LOCALIZATION NOTE (storage.data.label):
 # This is the heading displayed over the item value in the sidebar
 storage.data.label=Data
 
 # LOCALIZATION NOTE (storage.parsedValue.label):
 # This is the heading displayed over the item parsed value in the sidebar
 storage.parsedValue.label=Parsed Value
+
+# LOCALIZATION NOTE (storage.popupMenu.deleteLabel):
+# Label of popup menu action to delete storage item.
+storage.popupMenu.deleteLabel=Delete ā€œ%Sā€
--- a/devtools/client/shared/widgets/TableWidget.js
+++ b/devtools/client/shared/widgets/TableWidget.js
@@ -52,31 +52,34 @@ const MAX_VISIBLE_STRING_SIZE = 100;
  *                          the table. See @setupColumns for more info.
  *        - uniqueId: the column which will be the unique identifier of each
  *                    entry in the table. Default: name.
  *        - emptyText: text to display when no entries in the table to display.
  *        - highlightUpdated: true to highlight the changed/added row.
  *        - removableColumns: Whether columns are removeable. If set to false,
  *                            the context menu in the headers will not appear.
  *        - firstColumn: key of the first column that should appear.
+ *        - cellContextMenuId: ID of a <menupopup> element to be set as a
+ *                             context menu of every cell.
  */
 function TableWidget(node, options = {}) {
   EventEmitter.decorate(this);
 
   this.document = node.ownerDocument;
   this.window = this.document.defaultView;
   this._parent = node;
 
   let {initialColumns, emptyText, uniqueId, highlightUpdated, removableColumns,
-       firstColumn} = options;
+       firstColumn, cellContextMenuId} = options;
   this.emptyText = emptyText || "";
   this.uniqueId = uniqueId || "name";
   this.firstColumn = firstColumn || "";
   this.highlightUpdated = highlightUpdated || false;
   this.removableColumns = removableColumns !== false;
+  this.cellContextMenuId = cellContextMenuId;
 
   this.tbody = this.document.createElementNS(XUL_NS, "hbox");
   this.tbody.className = "table-widget-body theme-body";
   this.tbody.setAttribute("flex", "1");
   this.tbody.setAttribute("tabindex", "0");
   this._parent.appendChild(this.tbody);
   this.afterScroll = this.afterScroll.bind(this);
   this.tbody.addEventListener("scroll", this.onScroll.bind(this));
@@ -802,16 +805,17 @@ TableWidget.prototype = {
     }
     let removed = this.items.delete(item[this.uniqueId]);
 
     if (!removed) {
       return;
     }
     for (let column of this.columns.values()) {
       column.remove(item);
+      column.updateZebra();
     }
     if (this.items.size == 0) {
       this.tbody.setAttribute("empty", "empty");
     }
 
     this.emit(EVENTS.ROW_REMOVED, item);
   },
 
@@ -1444,16 +1448,25 @@ function Cell(column, item, nextCell) {
   this.label.className = "plain table-widget-cell";
 
   if (nextCell) {
     column.column.insertBefore(this.label, nextCell.label);
   } else {
     column.column.appendChild(this.label);
   }
 
+  if (column.table.cellContextMenuId) {
+    this.label.setAttribute("context", column.table.cellContextMenuId);
+    this.label.addEventListener("contextmenu", (event) => {
+      // Make the ID of the clicked cell available as a property on the table.
+      // It's then available for the popupshowing or command handler.
+      column.table.contextMenuRowId = this.id;
+    }, false);
+  }
+
   this.value = item[column.id];
   this.id = item[column.uniqueId];
 }
 
 Cell.prototype = {
 
   set id(value) {
     this._id = value;
--- a/devtools/client/storage/storage.xul
+++ b/devtools/client/storage/storage.xul
@@ -17,16 +17,22 @@
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/shared/theme-switching.js"/>
   <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
 
   <commandset id="editMenuCommands"/>
 
+  <popupset id="storagePopupSet">
+    <menupopup id="storage-table-popup">
+      <menuitem id="storage-table-popup-delete"/>
+    </menupopup>
+  </popupset>
+
   <box flex="1" class="devtools-responsive-container theme-body">
     <vbox id="storage-tree"/>
     <splitter class="devtools-side-splitter"/>
     <vbox flex="1">
       <hbox id="storage-toolbar" class="devtools-toolbar">
         <textbox id="storage-searchbox"
                  class="devtools-searchinput"
                  type="search"
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -15,14 +15,15 @@ support-files =
   head.js
 
 [browser_storage_basic.js]
 [browser_storage_cookies_edit.js]
 [browser_storage_cookies_edit_keyboard.js]
 [browser_storage_cookies_tab_navigation.js]
 [browser_storage_dynamic_updates.js]
 [browser_storage_localstorage_edit.js]
+[browser_storage_delete.js]
 [browser_storage_overflow.js]
 [browser_storage_search.js]
 skip-if = os == "linux" && e10s # Bug 1240804 - unhandled promise rejections
 [browser_storage_sessionstorage_edit.js]
 [browser_storage_sidebar.js]
 [browser_storage_values.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete.js
@@ -0,0 +1,49 @@
+/* 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/. */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+// Test deleting storage items
+
+const TEST_CASES = [
+  [["localStorage", "http://test1.example.org"],
+    "ls1", "name"],
+  [["sessionStorage", "http://test1.example.org"],
+    "ss1", "name"],
+  [["cookies", "test1.example.org"],
+    "c1", "name"]
+];
+
+add_task(function*() {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+  let contextMenu = gPanelWindow.document.getElementById("storage-table-popup");
+  let menuDeleteItem = contextMenu.querySelector("#storage-table-popup-delete");
+
+  for (let [ [store, host], rowName, cellToClick] of TEST_CASES) {
+    info(`Selecting tree item ${store} > ${host}`);
+    yield selectTreeItem([store, host]);
+
+    let row = getRowCells(rowName);
+
+    ok(gUI.table.items.has(rowName),
+      `There is a row '${rowName}' in ${store} > ${host}`);
+
+    yield waitForContextMenu(contextMenu, row[cellToClick], () => {
+      info(`Opened context menu in ${store} > ${host}, row '${rowName}'`);
+      menuDeleteItem.click();
+      ok(menuDeleteItem.getAttribute("label").includes(rowName),
+        `Context menu item label contains '${rowName}'`);
+    });
+
+    yield gUI.once("store-objects-updated");
+
+    ok(!gUI.table.items.has(rowName),
+      `There is no row '${rowName}' in ${store} > ${host} after deletion`);
+  }
+
+  yield finishTests();
+});
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -798,8 +798,55 @@ function getCurrentEditorValue() {
  * @param {Object} modifiers
  *         The event modifier e.g. {shiftKey: true}
  */
 function PressKeyXTimes(key, x, modifiers = {}) {
   for (let i = 0; i < x; i++) {
     EventUtils.synthesizeKey(key, modifiers);
   }
 }
+
+/**
+ * Wait for a context menu popup to open.
+ *
+ * @param nsIDOMElement popup
+ *        The XUL popup you expect to open.
+ * @param nsIDOMElement button
+ *        The button/element that receives the contextmenu event. This is
+ *        expected to open the popup.
+ * @param function onShown
+ *        Function to invoke on popupshown event.
+ * @param function onHidden
+ *        Function to invoke on popuphidden event.
+ * @return object
+ *         A Promise object that is resolved after the popuphidden event
+ *         callback is invoked.
+ */
+function waitForContextMenu(popup, button, onShown, onHidden) {
+  let deferred = promise.defer();
+
+  function onPopupShown() {
+    info("onPopupShown");
+    popup.removeEventListener("popupshown", onPopupShown);
+
+    onShown && onShown();
+
+    // Use executeSoon() to get out of the popupshown event.
+    popup.addEventListener("popuphidden", onPopupHidden);
+    executeSoon(() => popup.hidePopup());
+  }
+  function onPopupHidden() {
+    info("onPopupHidden");
+    popup.removeEventListener("popuphidden", onPopupHidden);
+
+    onHidden && onHidden();
+
+    deferred.resolve(popup);
+  }
+
+  popup.addEventListener("popupshown", onPopupShown);
+
+  info("wait for the context menu to open");
+  let eventDetails = {type: "contextmenu", button: 2};
+  EventUtils.synthesizeMouse(button, 2, 2, eventDetails,
+                             button.ownerDocument.defaultView);
+  return deferred.promise;
+}
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -42,16 +42,20 @@ const HIDDEN_COLUMNS = [
 
 const REASON = {
   NEW_ROW: "new-row",
   NEXT_50_ITEMS: "next-50-items",
   POPULATE: "populate",
   UPDATE: "update"
 };
 
+// Maximum length of item name to show in context menu label - will be
+// trimmed with ellipsis if it's longer.
+const ITEM_NAME_MAX_LENGTH = 32;
+
 /**
  * StorageUI is controls and builds the UI of the Storage Inspector.
  *
  * @param {Front} front
  *        Front for the storage actor
  * @param {Target} target
  *        Interface for the page we're debugging
  * @param {Window} panelWin
@@ -69,16 +73,17 @@ var StorageUI = this.StorageUI = functio
   this.tree = new TreeWidget(treeNode, {defaultType: "dir"});
   this.onHostSelect = this.onHostSelect.bind(this);
   this.tree.on("select", this.onHostSelect);
 
   let tableNode = this._panelDoc.getElementById("storage-table");
   this.table = new TableWidget(tableNode, {
     emptyText: L10N.getStr("table.emptyText"),
     highlightUpdated: true,
+    cellContextMenuId: "storage-table-popup"
   });
 
   this.displayObjectSidebar = this.displayObjectSidebar.bind(this);
   this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar);
 
   this.handleScrollEnd = this.handleScrollEnd.bind(this);
   this.table.on(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd);
 
@@ -100,16 +105,25 @@ var StorageUI = this.StorageUI = functio
 
   this.onUpdate = this.onUpdate.bind(this);
   this.front.on("stores-update", this.onUpdate);
   this.onCleared = this.onCleared.bind(this);
   this.front.on("stores-cleared", this.onCleared);
 
   this.handleKeypress = this.handleKeypress.bind(this);
   this._panelDoc.addEventListener("keypress", this.handleKeypress);
+
+  this.onPopupShowing = this.onPopupShowing.bind(this);
+  this._tablePopup = this._panelDoc.getElementById("storage-table-popup");
+  this._tablePopup.addEventListener("popupshowing", this.onPopupShowing, false);
+
+  this.onRemoveItem = this.onRemoveItem.bind(this);
+  this._tablePopupDelete = this._panelDoc.getElementById(
+    "storage-table-popup-delete");
+  this._tablePopupDelete.addEventListener("command", this.onRemoveItem, false);
 };
 
 exports.StorageUI = StorageUI;
 
 StorageUI.prototype = {
 
   storageTypes: null,
   shouldResetColumns: true,
@@ -125,16 +139,19 @@ StorageUI.prototype = {
     this.table.off(TableWidget.EVENTS.CELL_EDIT, this.editItem);
     this.table.destroy();
 
     this.front.off("stores-update", this.onUpdate);
     this.front.off("stores-cleared", this.onCleared);
     this._panelDoc.removeEventListener("keypress", this.handleKeypress);
     this.searchBox.removeEventListener("input", this.filterItems);
     this.searchBox = null;
+
+    this._tablePopup.removeEventListener("popupshowing", this.onPopupShowing);
+    this._tablePopupDelete.removeEventListener("command", this.onRemoveItem);
   },
 
   /**
    * Empties and hides the object viewer sidebar
    */
   hideSidebar: function() {
     this.view.empty();
     this.sidebar.hidden = true;
@@ -728,10 +745,46 @@ StorageUI.prototype = {
 
     let item = this.tree.selectedItem;
     let [type, host] = item;
     let names = null;
     if (item.length > 2) {
       names = [JSON.stringify(item.slice(2))];
     }
     this.fetchStorageObjects(type, host, names, REASON.NEXT_50_ITEMS);
-  }
+  },
+
+  /**
+   * Fires before a cell context menu with the "Delete" action is shown.
+   * If the current storage actor doesn't support removing items, prevent
+   * showing the menu.
+   */
+  onPopupShowing: function(event) {
+    if (!this.getCurrentActor().removeItem) {
+      event.preventDefault();
+      return;
+    }
+
+    let rowId = this.table.contextMenuRowId;
+    let data = this.table.items.get(rowId);
+    let name = data[this.table.uniqueId];
+
+    const maxLen = ITEM_NAME_MAX_LENGTH;
+    if (name.length > maxLen) {
+      name = name.substr(0, maxLen) + L10N.ellipsis;
+    }
+
+    this._tablePopupDelete.setAttribute("label",
+      L10N.getFormatStr("storage.popupMenu.deleteLabel", name));
+  },
+
+  /**
+   * Handles removing an item from the storage
+   */
+  onRemoveItem: function() {
+    let [, host] = this.tree.selectedItem;
+    let actor = this.getCurrentActor();
+    let rowId = this.table.contextMenuRowId;
+    let data = this.table.items.get(rowId);
+
+    actor.removeItem(host, data[this.table.uniqueId]);
+  },
 };
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -421,17 +421,17 @@ StorageActors.defaults = function(typeNa
  *         - typeName {string}
  *                    The typeName of the actor.
  *         - observationTopic {string}
  *                            The topic which this actor listens to via
  *                            Notification Observers.
  *         - storeObjectType {string}
  *                           The RetVal type of the store object of this actor.
  * @param {object} overrides
- *        All the methods which you want to be differnt from the ones in
+ *        All the methods which you want to be different from the ones in
  *        StorageActors.defaults method plus the required ones described there.
  */
 StorageActors.createActor = function(options = {}, overrides = {}) {
   let actorObject = StorageActors.defaults(
     options.typeName,
     options.observationTopic || null,
     options.storeObjectType
   );
@@ -669,24 +669,35 @@ StorageActors.createActor({
     this.editCookie(data);
   }), {
     request: {
       data: Arg(0, "json"),
     },
     response: {}
   }),
 
+  removeItem: method(Task.async(function*(host, name) {
+    this.removeCookie(host, name);
+  }), {
+    request: {
+      host: Arg(0),
+      name: Arg(1),
+    },
+    response: {}
+  }),
+
   maybeSetupChildProcess: function() {
     cookieHelpers.onCookieChanged = this.onCookieChanged.bind(this);
 
     if (!DebuggerServer.isInChildProcess) {
       this.getCookiesFromHost = cookieHelpers.getCookiesFromHost;
       this.addCookieObservers = cookieHelpers.addCookieObservers;
       this.removeCookieObservers = cookieHelpers.removeCookieObservers;
       this.editCookie = cookieHelpers.editCookie;
+      this.removeCookie = cookieHelpers.removeCookie;
       return;
     }
 
     const { sendSyncMessage, addMessageListener } =
       this.conn.parentMessageManager;
 
     this.conn.setupInParent({
       module: "devtools/server/actors/storage",
@@ -696,16 +707,18 @@ StorageActors.createActor({
     this.getCookiesFromHost =
       callParentProcess.bind(null, "getCookiesFromHost");
     this.addCookieObservers =
       callParentProcess.bind(null, "addCookieObservers");
     this.removeCookieObservers =
       callParentProcess.bind(null, "removeCookieObservers");
     this.editCookie =
       callParentProcess.bind(null, "editCookie");
+    this.removeCookie =
+      callParentProcess.bind(null, "removeCookie");
 
     addMessageListener("storage:storage-cookie-request-child",
                        cookieHelpers.handleParentRequest);
 
     function callParentProcess(methodName, ...args) {
       let reply = sendSyncMessage("storage:storage-cookie-request-parent", {
         method: methodName,
         args: args
@@ -849,16 +862,42 @@ var cookieHelpers = {
       cookie.value,
       cookie.isSecure,
       cookie.isHttpOnly,
       cookie.isSession,
       cookie.isSession ? MAX_COOKIE_EXPIRY : cookie.expires
     );
   },
 
+  removeCookie: function(host, name) {
+    function hostMatches(cookieHost, matchHost) {
+      if (cookieHost == null) {
+        return matchHost == null;
+      }
+      if (cookieHost.startsWith(".")) {
+        return matchHost.endsWith(cookieHost);
+      }
+      return cookieHost == host;
+    }
+
+    let enumerator = Services.cookies.getCookiesFromHost(host);
+    while (enumerator.hasMoreElements()) {
+      let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+      if (hostMatches(cookie.host, host) && cookie.name === name) {
+        Services.cookies.remove(
+          cookie.host,
+          cookie.name,
+          cookie.path,
+          cookie.originAttributes,
+          false
+        );
+      }
+    }
+  },
+
   addCookieObservers: function() {
     Services.obs.addObserver(cookieHelpers, "cookie-changed", false);
     return null;
   },
 
   removeCookieObservers: function() {
     Services.obs.removeObserver(cookieHelpers, "cookie-changed", false);
     return null;
@@ -897,27 +936,36 @@ var cookieHelpers = {
         cookie = JSON.parse(cookie);
         cookieHelpers.onCookieChanged(cookie, topic, data);
         break;
     }
   },
 
   handleChildRequest: function(msg) {
     switch (msg.json.method) {
-      case "getCookiesFromHost":
+      case "getCookiesFromHost": {
         let host = msg.data.args[0];
         let cookies = cookieHelpers.getCookiesFromHost(host);
         return JSON.stringify(cookies);
-      case "addCookieObservers":
+      }
+      case "addCookieObservers": {
         return cookieHelpers.addCookieObservers();
-      case "removeCookieObservers":
+      }
+      case "removeCookieObservers": {
         return cookieHelpers.removeCookieObservers();
-      case "editCookie":
+      }
+      case "editCookie": {
         let rowdata = msg.data.args[0];
         return cookieHelpers.editCookie(rowdata);
+      }
+      case "removeCookie": {
+        let host = msg.data.args[0];
+        let name = msg.data.args[1];
+        return cookieHelpers.removeCookie(host, name);
+      }
       default:
         console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method);
         throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD");
     }
   },
 };
 
 /**
@@ -1108,16 +1156,27 @@ function getObjectForLocalOrSessionStora
         return null;
       }
 
       return {
         name: item.name,
         value: new LongStringActor(this.conn, item.value || "")
       };
     },
+
+    removeItem: method(Task.async(function*(host, name) {
+      let storage = this.hostVsStores.get(host);
+      storage.removeItem(name);
+    }), {
+      request: {
+        host: Arg(0),
+        name: Arg(1),
+      },
+      response: {}
+    }),
   };
 }
 
 /**
  * The Local Storage actor and front.
  */
 StorageActors.createActor({
   typeName: "localStorage",
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -519,18 +519,19 @@ SendPing(void* aClosure, nsIContent* aCo
                 nullptr, // aCallbacks
                 nsIRequest::LOAD_NORMAL, // aLoadFlags,
                 aIOService);
 
   if (!chan) {
     return;
   }
 
-  // Don't bother caching the result of this URI load.
-  chan->SetLoadFlags(nsIRequest::INHIBIT_CACHING);
+  // Don't bother caching the result of this URI load, but do not exempt
+  // it from Safe Browsing.
+  chan->SetLoadFlags(nsIRequest::INHIBIT_CACHING | nsIChannel::LOAD_CLASSIFY_URI);
 
   nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(chan);
   if (!httpChan) {
     return;
   }
 
   // This is needed in order for 3rd-party cookie blocking to work.
   nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(httpChan);
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -1,12 +1,13 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/app"
 
 apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
 apply plugin: 'com.android.application'
+apply plugin: 'checkstyle'
 
 android {
     compileSdkVersion 23
     buildToolsVersion "23.0.1"
 
     defaultConfig {
         targetSdkVersion 23
         minSdkVersion 15 
@@ -206,16 +207,28 @@ dependencies {
     testCompile 'org.robolectric:robolectric:3.0'
     testCompile 'org.simpleframework:simple-http:6.0.1'
     testCompile 'org.mockito:mockito-core:1.10.19'
 
     // Including the Robotium JAR directly can cause issues with dexing.
     androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.5.4'
 }
 
+// TODO: (bug 1261486): This impl is not robust -
+// we just wanted to land something.
+task checkstyle(type: Checkstyle) {
+    configFile file("checkstyle.xml")
+    // TODO: should use sourceSets from project instead of hard-coded str.
+    source '../base/java/'
+    // TODO: This ignores our pre-processed resources.
+    include '**/*.java'
+    // TODO: classpath should probably be something.
+    classpath = files()
+}
+
 task syncOmnijarFromDistDir(type: Sync) {
     into("${project.buildDir}/generated/omnijar")
     from("${topobjdir}/dist/fennec/assets") {
         include 'omni.ja'
     }
 }
 
 task checkLibsExistInDistDir<< {
new file mode 100644
--- /dev/null
+++ b/mobile/android/app/checkstyle.xml
@@ -0,0 +1,49 @@
+<?xml version="1.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/. -->
+<!DOCTYPE module PUBLIC
+    "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+    "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<!-- TODO: Clean up code & add checks:
+       - WhitespaceAround
+       - EmptyLineSeparator
+       - NeedBraces
+       - LeftCurly // placement of "{" in new scope or literal
+       - RightCurly // placement of "}" in close scope or literal
+       - Indentation
+       - OneStatementPerLine
+       - OperatorWrap
+       - SeparatorWrap
+       - MultipleVariableDeclarations
+       - FallThrough
+       (I spent too much time already)
+
+     Maybe add:
+       - OneTopLevelClass
+       - OverloadMethodsDeclarationOrder
+       - Empty*Block // better to catch errors!
+       (I spent too much time already)
+
+     See http://checkstyle.sourceforge.net/google_style.html
+     for a good set of defaults.
+-->
+
+<module name="Checker">
+    <property name="charset" value="UTF-8"/>
+
+    <!-- TODO: <property name="fileExtensions" value="java, properties, xml"/> -->
+
+    <module name="FileTabCharacter"> <!-- No tabs! -->
+        <property name="eachLine" value="true"/>
+    </module>
+
+    <module name="TreeWalker">
+        <module name="NoLineWrap">
+            <property name="tokens" value="IMPORT,PACKAGE_DEF"/>
+        </module>
+        <module name="OuterTypeFilename"/> <!-- `class Lol` only in Lol.java -->
+    </module>
+
+</module>
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -958,35 +958,16 @@ OnSharedPreferenceChangeListener
         return sIsCharEncodingEnabled;
     }
 
     public static void broadcastAction(final Context context, final Intent intent) {
         fillIntentWithProfileInfo(context, intent);
         context.sendBroadcast(intent, GlobalConstants.PER_ANDROID_PACKAGE_PERMISSION);
     }
 
-    /**
-     * Broadcast an intent with <code>pref</code>, <code>branch</code>, and
-     * <code>enabled</code> extras. This is intended to represent the
-     * notification of a preference value to observers.
-     *
-     * The broadcast will be sent only to receivers registered with the
-     * (Fennec-specific) per-Android package permission.
-     */
-    public static void broadcastPrefAction(final Context context,
-                                           final String action,
-                                           final String pref,
-                                           final boolean value) {
-        final Intent intent = new Intent(action)
-                .putExtra("pref", pref)
-                .putExtra("branch", GeckoSharedPrefs.APP_PREFS_NAME)
-                .putExtra("enabled", value);
-        broadcastAction(context, intent);
-    }
-
     private static void fillIntentWithProfileInfo(final Context context, final Intent intent) {
         // There is a race here, but GeckoProfile returns the default profile
         // when Gecko is not explicitly running for a different profile.  In a
         // multi-profile world, this will need to be updated (possibly to
         // broadcast settings for all profiles).  See Bug 882182.
         GeckoProfile profile = GeckoProfile.get(context);
         if (profile != null) {
             intent.putExtra("profileName", profile.getName())
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -145,16 +145,17 @@
 <!ENTITY pref_category_search_summary "Customize your search providers">
 <!ENTITY pref_category_accessibility "Accessibility">
 <!ENTITY pref_category_accessibility_summary2 "Text size, zoom, voice input">
 <!ENTITY pref_category_privacy_short "Privacy">
 <!ENTITY pref_category_privacy_summary3 "Tracking, cookies, data choices">
 <!ENTITY pref_category_vendor2 "&vendorShortName; &brandShortName;">
 <!ENTITY pref_category_vendor_summary2 "About &brandShortName;, FAQs, feedback">
 <!ENTITY pref_category_datareporting "Data choices">
+<!ENTITY pref_category_logins "Logins">
 <!ENTITY pref_learn_more "Learn more">
 <!ENTITY pref_category_installed_search_engines "Installed search engines">
 <!ENTITY pref_category_add_search_providers "Add more search providers">
 <!ENTITY pref_category_search_restore_defaults "Restore search engines">
 <!ENTITY pref_search_restore_defaults "Restore defaults">
 <!ENTITY pref_search_restore_defaults_summary "Restore defaults">
 <!-- Localization note (pref_search_hint) : "TIP" as in "hint", "clue" etc. Displayed as an
      advisory message on the customise search providers settings page explaining how to add new
--- a/mobile/android/base/resources/xml/preferences_privacy.xml
+++ b/mobile/android/base/resources/xml/preferences_privacy.xml
@@ -37,42 +37,49 @@
             url="https://support.mozilla.org/kb/firefox-android-tracking-protection" />
 
     <ListPreference android:key="network.cookie.cookieBehavior"
                     android:title="@string/pref_cookies_menu"
                     android:entries="@array/pref_cookies_entries"
                     android:entryValues="@array/pref_cookies_values"
                     android:persistent="false" />
 
-    <org.mozilla.gecko.preferences.LinkPreference android:key="android.not_a_preference.signon.manage"
-                                                  android:title="@string/pref_manage_logins"
-                                                  url="about:logins"/>
-
-    <CheckBoxPreference android:key="signon.rememberSignons"
-                        android:title="@string/pref_remember_signons"
-                        android:persistent="false" />
-
-    <CheckBoxPreference android:key="privacy.masterpassword.enabled"
-                        android:title="@string/pref_use_master_password"
-                        android:persistent="false" />
-
 
     <!-- This pref is persisted in both Gecko and Java -->
     <org.mozilla.gecko.preferences.ListCheckboxPreference
                         android:key="android.not_a_preference.history.clear_on_exit"
                         gecko:entries="@array/pref_private_data_entries"
                         gecko:entryValues="@array/pref_private_data_values"
                         gecko:initialValues="@array/pref_clear_on_exit_defaults"
 
                         android:title="@string/pref_clear_on_exit_title"
                         android:summary="@string/pref_clear_on_exit_summary2"
 
                         android:dialogTitle="@string/pref_clear_on_exit_dialog_title"
                         android:positiveButtonText="@string/button_set"/>
 
+    <PreferenceCategory android:title="@string/pref_category_logins">
+
+        <org.mozilla.gecko.preferences.LinkPreference
+            android:key="android.not_a_preference.signon.manage"
+            android:title="@string/pref_manage_logins"
+            url="about:logins"/>
+
+        <CheckBoxPreference
+            android:key="signon.rememberSignons"
+            android:title="@string/pref_remember_signons"
+            android:persistent="false" />
+
+        <CheckBoxPreference
+            android:key="privacy.masterpassword.enabled"
+            android:title="@string/pref_use_master_password"
+            android:persistent="false" />
+
+    </PreferenceCategory>
+
     <PreferenceCategory android:key="android.not_a_preference.datareporting.preferences"
                         android:title="@string/pref_category_datareporting">
 
         <CheckBoxPreference android:key="toolkit.telemetry.enabled"
                             android:title="@string/datareporting_telemetry_title"
                             android:summary="@string/datareporting_telemetry_summary" />
 
         <CheckBoxPreference android:key="datareporting.crashreporter.submitEnabled"
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -141,16 +141,17 @@
   <string name="pref_category_search_summary">&pref_category_search_summary;</string>
   <string name="pref_category_accessibility">&pref_category_accessibility;</string>
   <string name="pref_category_accessibility_summary">&pref_category_accessibility_summary2;</string>
   <string name="pref_category_privacy_short">&pref_category_privacy_short;</string>
   <string name="pref_category_privacy_summary">&pref_category_privacy_summary3;</string>
   <string name="pref_category_vendor">&pref_category_vendor2;</string>
   <string name="pref_category_vendor_summary">&pref_category_vendor_summary2;</string>
   <string name="pref_category_datareporting">&pref_category_datareporting;</string>
+  <string name="pref_category_logins">&pref_category_logins;</string>
   <string name="pref_category_installed_search_engines">&pref_category_installed_search_engines;</string>
   <string name="pref_category_add_search_providers">&pref_category_add_search_providers;</string>
   <string name="pref_category_search_restore_defaults">&pref_category_search_restore_defaults;</string>
   <string name="pref_search_restore_defaults">&pref_search_restore_defaults;</string>
   <string name="pref_search_restore_defaults_summary">&pref_search_restore_defaults_summary;</string>
   <string name="pref_search_hint">&pref_search_hint2;</string>
 
   <string name="pref_category_language">&pref_category_language;</string>
--- a/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.cpp
+++ b/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.cpp
@@ -20,16 +20,19 @@
 #include "mozilla/Logging.h"
 #include "nsIInterfaceRequestor.h"
 #include "mozilla/LoadContext.h"
 #include "mozilla/Telemetry.h"
 #include "nsContentUtils.h"
 
 static const char* gQuitApplicationMessage = "quit-application";
 
+// Limit the list file size to 32mb
+const uint32_t MAX_FILE_SIZE = (32 * 1024 * 1024);
+
 #undef LOG
 
 // NSPR_LOG_MODULES=UrlClassifierStreamUpdater:5
 static mozilla::LazyLogModule gUrlClassifierStreamUpdaterLog("UrlClassifierStreamUpdater");
 #define LOG(args) MOZ_LOG(gUrlClassifierStreamUpdaterLog, mozilla::LogLevel::Debug, args)
 
 // This class does absolutely nothing, except pass requests onto the DBService.
 
@@ -639,16 +642,21 @@ nsUrlClassifierStreamUpdater::OnDataAvai
                                               uint64_t aSourceOffset,
                                               uint32_t aLength)
 {
   if (!mDBService)
     return NS_ERROR_NOT_INITIALIZED;
 
   LOG(("OnDataAvailable (%d bytes)", aLength));
 
+  if (aSourceOffset > MAX_FILE_SIZE) {
+    LOG(("OnDataAvailable::Abort because exceeded the maximum file size(%lld)", aSourceOffset));
+    return NS_ERROR_FILE_TOO_BIG;
+  }
+
   nsresult rv;
 
   // Copy the data into a nsCString
   nsCString chunk;
   rv = NS_ConsumeStream(aIStream, aLength, chunk);
   NS_ENSURE_SUCCESS(rv, rv);
 
   //LOG(("Chunk (%d): %s\n\n", chunk.Length(), chunk.get()));
--- a/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
+++ b/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
@@ -12,12 +12,14 @@ support-files =
   evilWorker.js
   import.css
   raptor.jpg
   track.html
   unwantedWorker.js
   vp9.webm
   whitelistFrame.html
   workerFrame.html
+  ping.sjs
 
 [test_classifier.html]
 skip-if = (os == 'linux' && debug) #Bug 1199778
 [test_classifier_worker.html]
+[test_classify_ping.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/ping.sjs
@@ -0,0 +1,16 @@
+function handleRequest(request, response)
+{
+  var query = {};
+  request.queryString.split('&').forEach(function (val) {
+    var [name, value] = val.split('=');
+    query[name] = unescape(value);
+  });
+
+  if (request.method == "POST") {
+    setState(query["id"], "ping");
+  } else {
+    var value = getState(query["id"]);
+    response.setHeader("Content-Type", "text/plain", false);
+    response.write(value);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_classify_ping.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Bug 1233914 - ping doesn't honor the TP list here.</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+  SimpleTest.requestFlakyTimeout("Delay to make sure ping is made prior than XHR");
+
+  const timeout = 200;
+  const host_nottrack = "http://not-tracking.example.com/";
+  const host_track = "http://trackertest.org/";
+  const path_ping = "tests/toolkit/components/url-classifier/tests/mochitest/ping.sjs";
+  const TP_ENABLE_PREF = "privacy.trackingprotection.enabled";
+
+  function testPingNonBlacklist() {
+    SpecialPowers.setBoolPref(TP_ENABLE_PREF, true);
+
+    var msg = "ping should reach page not in blacklist";
+    var expectPing = true;
+    var id = "1111";
+    ping(id, host_nottrack);
+
+    return new Promise(function(resolve, reject) {
+      setTimeout(function() {
+        isPinged(id, expectPing, msg, resolve);
+      }, timeout);
+    });
+  }
+
+  function testPingBlacklistSafebrowsingOff() {
+    SpecialPowers.setBoolPref(TP_ENABLE_PREF, false);
+
+    var msg = "ping should reach page in blacklist when tracking protection is off";
+    var expectPing = true;
+    var id = "2222";
+    ping(id, host_track);
+
+    return new Promise(function(resolve, reject) {
+      setTimeout(function() {
+        isPinged(id, expectPing, msg, resolve);
+      }, timeout);
+    });
+  }
+
+  function testPingBlacklistSafebrowsingOn() {
+    SpecialPowers.setBoolPref(TP_ENABLE_PREF, true);
+
+    var msg = "ping should not reach page in blacklist when tracking protection is on";
+    var expectPing = false;
+    var id = "3333";
+    ping(id, host_track);
+
+    return new Promise(function(resolve, reject) {
+      setTimeout(function() {
+        isPinged(id, expectPing, msg, resolve);
+      }, timeout);
+    });
+  }
+
+  function ping(id, host) {
+    var elm = document.createElement("a");
+    elm.setAttribute('ping', host + path_ping + "?id=" + id);
+    elm.setAttribute('href', "#");
+    document.body.appendChild(elm);
+
+    // Trigger ping.
+    elm.click();
+
+    document.body.removeChild(elm);
+  }
+
+  function isPinged(id, expected, msg, callback) {
+    var url = "http://mochi.test:8888/" + path_ping;
+    var xhr = new XMLHttpRequest();
+    xhr.open('GET', url + "?id=" + id);
+    xhr.onload = function() {
+      var isPinged = xhr.response === "ping";
+      is(expected, isPinged, msg);
+
+      callback();
+    };
+    xhr.send();
+  }
+
+  function cleanup() {
+    SpecialPowers.clearUserPref(TP_ENABLE_PREF);
+  }
+
+  function runTest() {
+    Promise.resolve()
+      .then(testPingNonBlacklist)
+      .then(testPingBlacklistSafebrowsingOff)
+      .then(testPingBlacklistSafebrowsingOn)
+      .then(function() {
+        SimpleTest.finish();
+      }).catch(function(e) {
+        ok(false, "Some test failed with error " + e);
+        SimpleTest.finish();
+      });
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  SimpleTest.registerCleanupFunction(cleanup);
+  SpecialPowers.pushPrefEnv({"set": [
+    ["browser.send_pings", true],
+    ["urlclassifier.trackingTable", "test-track-simple"],
+  ]}, runTest);
+
+</script>
+</pre>
+</body>
+</html>
--- a/toolkit/content/tests/browser/browser_input_file_tooltips.js
+++ b/toolkit/content/tests/browser/browser_input_file_tooltips.js
@@ -30,21 +30,23 @@ add_task(function* test_requiredset() {
   yield do_test({required: true, result: "Please select a file."});
 });
 
 function* do_test(test) {
   info(`starting test ${JSON.stringify(test)}`);
 
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
 
+  info("Moving mouse out of the way.");
   yield new Promise(resolve => {
     EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 300, 300, resolve);
   });
 
-  ContentTask.spawn(tab.linkedBrowser, test, function*(test) {
+  info("creating input field");
+  yield ContentTask.spawn(tab.linkedBrowser, test, function*(test) {
     let doc = content.document;
     let input = doc.createElement("input");
     doc.body.appendChild(input);
     input.id = "test_input";
     input.setAttribute("style", "position: absolute; top: 0; left: 0;");
     input.type = "file";
     if (test.title) {
       input.setAttribute("title", test.title);
@@ -53,58 +55,67 @@ function* do_test(test) {
       input.multiple = true;
     }
     if (test.required) {
       input.required = true;
     }
   });
 
   if (test.value) {
+    info("Creating mock filepicker to select files");
     let MockFilePicker = SpecialPowers.MockFilePicker;
     MockFilePicker.init(window);
     MockFilePicker.returnValue = MockFilePicker.returnOK;
     MockFilePicker.displayDirectory = FileUtils.getDir("TmpD", [], false);
     MockFilePicker.returnFiles = [tempFile];
 
     try {
       // Open the File Picker dialog (MockFilePicker) to select
       // the files for the test.
       yield BrowserTestUtils.synthesizeMouseAtCenter("#test_input", {}, tab.linkedBrowser);
+      info("Waiting for the input to have the requisite files");
       yield ContentTask.spawn(tab.linkedBrowser, {}, function*() {
         let input = content.document.querySelector("#test_input");
         yield ContentTaskUtils.waitForCondition(() => input.files.length,
           "The input should have at least one file selected");
         info(`The input has ${input.files.length} file(s) selected.`);
       });
     } finally {
       MockFilePicker.cleanup();
     }
+  } else {
+    info("No real file selection required.");
   }
 
   let awaitTooltipOpen = new Promise(resolve => {
     let tooltipId = Services.appinfo.browserTabsRemoteAutostart ?
                       "remoteBrowserTooltip" :
                       "aHTMLTooltip";
     let tooltip = document.getElementById(tooltipId);
     tooltip.addEventListener("popupshown", function onpopupshown(event) {
       tooltip.removeEventListener("popupshown", onpopupshown);
       resolve(event.target);
     });
   });
+  info("Initial mouse move");
   yield new Promise(resolve => {
     EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 100, 5, resolve);
   });
+  info("Waiting");
   yield new Promise(resolve => setTimeout(resolve, 100));
+  info("Second mouse move");
   yield new Promise(resolve => {
     EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 110, 15, resolve);
   });
+  info("Waiting for tooltip to open");
   let tooltip = yield awaitTooltipOpen;
 
   is(tooltip.getAttribute("label"), test.result, "tooltip label should match expectation");
 
+  info("Closing tab");
   yield BrowserTestUtils.removeTab(tab);
 }
 
 function createTempFile() {
   let file = FileUtils.getDir("TmpD", [], false);
   file.append("testfile_bug1251809");
   file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
   return file;