moebius#339: Make it possible to add cookies, local and session storage entries

Issue #31
https://github.com/MoonchildProductions/moebius/pull/339
This commit is contained in:
janekptacijarabaci 2018-03-02 17:52:34 +01:00 committed by Roy Tam
commit 6d865bfda3
13 changed files with 309 additions and 43 deletions

View file

@ -87,7 +87,11 @@ storage.parsedValue.label=Parsed Value
# Label of popup menu action to delete storage item.
storage.popupMenu.deleteLabel=Delete “%S”
# LOCALIZATION NOTE (storage.popupMenu.deleteAllLabel):
# LOCALIZATION NOTE (storage.popupMenu.addItemLabel):
# Label of popup menu action to add an item.
storage.popupMenu.addItemLabel=Add Item
# LOCALIZATION NOTE (storage.popupMenu.deleteAllFromLabel):
# Label of popup menu action to delete all storage items.
storage.popupMenu.deleteAllFromLabel=Delete All From “%S”

View file

@ -123,6 +123,8 @@ function TableWidget(node, options = {}) {
TableWidget.prototype = {
items: null,
editBookmark: null,
scrollIntoViewOnUpdate: null,
/**
* Getter for the headers context menu popup id.
@ -1159,7 +1161,9 @@ Column.prototype = {
},
/**
* Called when a row is updated.
* Called when a row is updated e.g. a cell is changed. This means that
* for a new row this method will be called once for each column. If a single
* cell is changed this method will be called just once.
*
* @param {string} event
* The event name of the event. i.e. EVENTS.ROW_UPDATED
@ -1168,7 +1172,23 @@ Column.prototype = {
*/
onRowUpdated: function (event, id) {
this._updateItems();
if (this.highlightUpdated && this.items[id] != null) {
if (this.table.scrollIntoViewOnUpdate) {
let cell = this.cells[this.items[id]];
// When a new row is created this method is called once for each column
// as each cell is updated. We can only scroll to cells if they are
// visible. We check for visibility and once we find the first visible
// cell in a row we scroll it into view and reset the
// scrollIntoViewOnUpdate flag.
if (cell.label.clientHeight > 0) {
cell.scrollIntoView();
this.table.scrollIntoViewOnUpdate = null;
}
}
if (this.table.editBookmark) {
// A rows position in the table can change as the result of an edit. In
// order to ensure that the correct row is highlighted after an edit we
@ -1180,6 +1200,7 @@ Column.prototype = {
this.cells[this.items[id]].flash();
}
this.updateZebra();
},
@ -1594,6 +1615,10 @@ Cell.prototype = {
this.label.focus();
},
scrollIntoView: function () {
this.label.scrollIntoView(false);
},
destroy: function () {
this.label.remove();
this.label = null;

View file

@ -31,6 +31,7 @@
<menuitem id="storage-tree-popup-delete"/>
</menupopup>
<menupopup id="storage-table-popup">
<menuitem id="storage-table-popup-add"/>
<menuitem id="storage-table-popup-delete"/>
<menuitem id="storage-table-popup-delete-all-from"/>
<menuitem id="storage-table-popup-delete-all"
@ -45,6 +46,9 @@
<splitter class="devtools-side-splitter"/>
<vbox flex="1">
<hbox id="storage-toolbar" class="devtools-toolbar">
<button id="add-button"
class="devtools-button add-button"></button>
<spacer flex="1"/>
<textbox id="storage-searchbox"
class="devtools-filterinput"
type="search"

View file

@ -22,6 +22,7 @@ support-files =
[browser_storage_basic.js]
[browser_storage_cache_delete.js]
[browser_storage_cache_error.js]
[browser_storage_cookies_add.js]
[browser_storage_cookies_delete_all.js]
[browser_storage_cookies_domain.js]
[browser_storage_cookies_edit.js]
@ -38,11 +39,13 @@ support-files =
[browser_storage_indexeddb_delete.js]
[browser_storage_indexeddb_delete_blocked.js]
[browser_storage_indexeddb_duplicate_names.js]
[browser_storage_localstorage_add.js]
[browser_storage_localstorage_edit.js]
[browser_storage_localstorage_error.js]
[browser_storage_overflow.js]
[browser_storage_search.js]
[browser_storage_search_keyboard_trap.js]
[browser_storage_sessionstorage_add.js]
[browser_storage_sessionstorage_edit.js]
[browser_storage_sidebar.js]
[browser_storage_sidebar_update.js]

View file

@ -0,0 +1,20 @@
/* 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/. */
// Basic test to check the adding of cookies.
"use strict";
add_task(function* () {
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
showAllColumns(true);
yield performAdd(["cookies", "http://test1.example.org"]);
yield performAdd(["cookies", "http://test1.example.org"]);
yield performAdd(["cookies", "http://test1.example.org"]);
yield performAdd(["cookies", "http://test1.example.org"]);
yield performAdd(["cookies", "http://test1.example.org"]);
yield finishTests();
});

View file

@ -18,8 +18,8 @@ add_task(function* () {
info("do the delete");
yield selectTreeItem(["indexedDB", "http://test1.example.org"]);
let actor = gUI.getCurrentActor();
let result = yield actor.removeDatabase("http://test1.example.org", "idb (default)");
let front = gUI.getCurrentFront();
let result = yield front.removeDatabase("http://test1.example.org", "idb (default)");
ok(result.blocked, "removeDatabase attempt is blocked");
@ -47,7 +47,7 @@ add_task(function* () {
info("try to delete database from nonexistent host");
let errorThrown = false;
try {
result = yield actor.removeDatabase("http://test2.example.org", "idb (default)");
result = yield front.removeDatabase("http://test2.example.org", "idb (default)");
} catch (ex) {
errorThrown = true;
}

View file

@ -0,0 +1,20 @@
/* 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/. */
// Basic test to check the adding of localStorage entries.
"use strict";
add_task(function* () {
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-localstorage.html");
showAllColumns(true);
yield performAdd(["localStorage", "http://test1.example.org"]);
yield performAdd(["localStorage", "http://test1.example.org"]);
yield performAdd(["localStorage", "http://test1.example.org"]);
yield performAdd(["localStorage", "http://test1.example.org"]);
yield performAdd(["localStorage", "http://test1.example.org"]);
yield finishTests();
});

View file

@ -0,0 +1,20 @@
/* 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/. */
// Basic test to check the adding of sessionStorage entries.
"use strict";
add_task(function* () {
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-sessionstorage.html");
showAllColumns(true);
yield performAdd(["sessionStorage", "http://test1.example.org"]);
yield performAdd(["sessionStorage", "http://test1.example.org"]);
yield performAdd(["sessionStorage", "http://test1.example.org"]);
yield performAdd(["sessionStorage", "http://test1.example.org"]);
yield performAdd(["sessionStorage", "http://test1.example.org"]);
yield finishTests();
});

View file

@ -897,3 +897,39 @@ function setPermission(url, permission) {
.addFromPrincipal(principal, permission,
nsIPermissionManager.ALLOW_ACTION);
}
/**
* Add an item.
* @param {Array} store
* An array containing the path to the store to which we wish to add an
* item.
*/
function* performAdd(store) {
let storeName = store.join(" > ");
let toolbar = gPanelWindow.document.getElementById("storage-toolbar");
let type = store[0];
yield selectTreeItem(store);
let menuAdd = toolbar.querySelector(
"#add-button");
if (menuAdd.hidden) {
is(menuAdd.hidden, false,
`performAdd called for ${storeName} but it is not supported`);
return;
}
let eventEdit = gUI.table.once("row-edit");
let eventWait = gUI.once("store-objects-updated");
menuAdd.click();
let rowId = yield eventEdit;
yield eventWait;
let key = type === "cookies" ? "uniqueKey" : "name";
let value = getCellValue(rowId, key);
is(rowId, value, `Row '${rowId}' was successfully added.`);
}

View file

@ -160,12 +160,20 @@ function StorageUI(front, target, panelWin, toolbox) {
this._tablePopup = this._panelDoc.getElementById("storage-table-popup");
this._tablePopup.addEventListener("popupshowing", this.onTablePopupShowing);
this.onAddItem = this.onAddItem.bind(this);
this.onRemoveItem = this.onRemoveItem.bind(this);
this.onRemoveAllFrom = this.onRemoveAllFrom.bind(this);
this.onRemoveAll = this.onRemoveAll.bind(this);
this.onRemoveAllSessionCookies = this.onRemoveAllSessionCookies.bind(this);
this.onRemoveTreeItem = this.onRemoveTreeItem.bind(this);
this._addButton = this._panelDoc.getElementById("add-button");
this._addButton.addEventListener("command", this.onAddItem);
this._tablePopupAddItem = this._panelDoc.getElementById(
"storage-table-popup-add");
this._tablePopupAddItem.addEventListener("command", this.onAddItem);
this._tablePopupDelete = this._panelDoc.getElementById(
"storage-table-popup-delete");
this._tablePopupDelete.addEventListener("command", this.onRemoveItem);
@ -221,6 +229,8 @@ StorageUI.prototype = {
this.searchBox = null;
this._treePopup.removeEventListener("popupshowing", this.onTreePopupShowing);
this._addButton.removeEventListener("command", this.onAddItem);
this._tablePopupAddItem.removeEventListener("command", this.onAddItem);
this._treePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
this._treePopupDeleteAllSessionCookies.removeEventListener("command",
this.onRemoveAllSessionCookies);
@ -243,7 +253,7 @@ StorageUI.prototype = {
this.table.clearSelection();
},
getCurrentActor: function () {
getCurrentFront: function () {
let type = this.table.datatype;
return this.storageTypes[type];
@ -264,9 +274,9 @@ StorageUI.prototype = {
},
editItem: function (eventType, data) {
let actor = this.getCurrentActor();
let front = this.getCurrentFront();
actor.editItem(data);
front.editItem(data);
},
/**
@ -520,7 +530,7 @@ StorageUI.prototype = {
// The indexedDB type could have sub-type data to fetch.
// If having names specified, then it means
// we are fetching details of specific database or of object store.
if (type == "indexedDB" && names) {
if (type === "indexedDB" && names) {
let [ dbName, objectStoreName ] = JSON.parse(names[0]);
if (dbName) {
subType = "database";
@ -529,6 +539,15 @@ StorageUI.prototype = {
subType = "object store";
}
}
this.actorSupportsAddItem = yield this._target.actorHasMethod(type, "addItem");
this.actorSupportsRemoveItem =
yield this._target.actorHasMethod(type, "removeItem");
this.actorSupportsRemoveAll =
yield this._target.actorHasMethod(type, "removeAll");
this.actorSupportsRemoveAllSessionCookies =
yield this._target.actorHasMethod(type, "removeAllSessionCookies");
yield this.resetColumns(type, host, subType);
}
@ -536,12 +555,34 @@ StorageUI.prototype = {
if (data.length) {
this.populateTable(data, reason);
}
yield this.updateToolbar();
this.emit("store-objects-updated");
} catch (ex) {
console.error(ex);
}
}),
/**
* Updates the toolbar hiding and showing buttons as appropriate.
*/
updateToolbar: Task.async(function* () {
let item = this.tree.selectedItem;
let howManyNodesIn = item ? item.length : 0;
// The first node is just a title e.g. "Cookies" so we need to be at least
// 2 nodes in to show the add button.
let canAdd = this.actorSupportsAddItem && howManyNodesIn > 1;
if (canAdd) {
this._addButton.hidden = false;
this._addButton.setAttribute("tooltiptext",
L10N.getFormatStr("storage.popupMenu.addItemLabel"));
} else {
this._addButton.hidden = true;
this._addButton.removeAttribute("tooltiptext");
}
}),
/**
* Populates the storage tree which displays the list of storages present for
* the page.
@ -777,11 +818,19 @@ StorageUI.prototype = {
* the storage tree
*/
onHostSelect: function (event, item) {
if (!item) {
return;
}
this.table.clear();
this.hideSidebar();
this.searchBox.value = "";
let [type, host] = item;
this.table.host = host;
this.table.datatype = type;
this.updateToolbar();
let names = null;
if (!host) {
return;
@ -814,7 +863,7 @@ StorageUI.prototype = {
let editableFields = [];
let hiddenFields = [];
let privateFields = [];
let fields = yield this.getCurrentActor().getFields(subtype);
let fields = yield this.getCurrentFront().getFields(subtype);
fields.forEach(f => {
if (!uniqueKey) {
@ -949,33 +998,47 @@ StorageUI.prototype = {
},
/**
* Fires before a cell context menu with the "Delete" action is shown.
* If the currently selected storage object doesn't support removing items, prevent
* showing the menu.
* Fires before a cell context menu with the "Add" or "Delete" action is
* shown. If the currently selected storage object doesn't support adding or
* removing items, prevent showing the menu.
*/
onTablePopupShowing: function (event) {
let selectedItem = this.tree.selectedItem;
let type = selectedItem[0];
let actor = this.getCurrentActor();
// IndexedDB only supports removing items from object stores (level 4 of the tree)
if (!actor.removeItem || (type === "indexedDB" && selectedItem.length !== 4)) {
if ((!this.actorSupportsAddItem && !this.actorSupportsRemoveItem &&
type !== "cookies") ||
(type === "indexedDB" && selectedItem.length !== 4)) {
event.preventDefault();
return;
}
let rowId = this.table.contextMenuRowId;
let data = this.table.items.get(rowId);
let name = data[this.table.uniqueId];
let separatorRegex = new RegExp(SEPARATOR_GUID, "g");
let label = addEllipsis((name + "").replace(separatorRegex, "-"));
if (this.actorSupportsRemoveItem) {
let name = data[this.table.uniqueId];
let separatorRegex = new RegExp(SEPARATOR_GUID, "g");
let label = addEllipsis((name + "").replace(separatorRegex, "-"));
this._tablePopupDelete.setAttribute("label",
L10N.getFormatStr("storage.popupMenu.deleteLabel", label));
this._tablePopupDelete.hidden = false;
this._tablePopupDelete.setAttribute("label",
L10N.getFormatStr("storage.popupMenu.deleteLabel", label));
} else {
this._tablePopupDelete.hidden = true;
}
if (this.actorSupportsAddItem) {
this._tablePopupAddItem.hidden = false;
this._tablePopupAddItem.setAttribute("label",
L10N.getFormatStr("storage.popupMenu.addItemLabel"));
} else {
this._tablePopupAddItem.hidden = true;
}
let showDeleteAllSessionCookies = false;
if (selectedItem && actor.removeAllSessionCookies) {
if (this.actorSupportsRemoveAllSessionCookies) {
if (type === "cookies" && selectedItem.length === 2) {
showDeleteAllSessionCookies = true;
}
@ -1000,13 +1063,12 @@ StorageUI.prototype = {
if (selectedItem) {
let type = selectedItem[0];
let actor = this.storageTypes[type];
// The delete all (aka clear) action is displayed for IndexedDB object stores
// (level 4 of tree), for Cache objects (level 3) and for the whole host (level 2)
// for other storage types (cookies, localStorage, ...).
let showDeleteAll = false;
if (actor.removeAll) {
if (this.actorSupportsRemoveAll) {
let level;
if (type == "indexedDB") {
level = 4;
@ -1026,7 +1088,7 @@ StorageUI.prototype = {
// The delete all session cookies action is displayed for cookie object stores
// (level 2 of tree)
let showDeleteAllSessionCookies = false;
if (actor.removeAllSessionCookies) {
if (this.actorSupportsRemoveAllSessionCookies) {
if (type === "cookies" && selectedItem.length === 2) {
showDeleteAllSessionCookies = true;
}
@ -1054,45 +1116,55 @@ StorageUI.prototype = {
}
},
/**
* Handles adding an item from the storage
*/
onAddItem: function () {
if (!this.tree.selectedItem) {
return;
}
let front = this.getCurrentFront();
let [, host] = this.tree.selectedItem;
// Prepare to scroll into view.
this.table.scrollIntoViewOnUpdate = true;
this.table.editBookmark = createGUID();
front.addItem(this.table.editBookmark, host);
},
/**
* Handles removing an item from the storage
*/
onRemoveItem: function () {
let [, host, ...path] = this.tree.selectedItem;
let actor = this.getCurrentActor();
let front = this.getCurrentFront();
let rowId = this.table.contextMenuRowId;
let data = this.table.items.get(rowId);
let name = data[this.table.uniqueId];
if (path.length > 0) {
name = JSON.stringify([...path, name]);
}
actor.removeItem(host, name);
front.removeItem(host, name);
},
/**
* Handles removing all items from the storage
*/
onRemoveAll: function () {
// Cannot use this.currentActor() if the handler is called from the
// tree context menu: it returns the correct value only after the
// table data from server are successfully fetched (and that's async).
let [type, host, ...path] = this.tree.selectedItem;
let actor = this.storageTypes[type];
let [, host, ...path] = this.tree.selectedItem;
let front = this.getCurrentFront();
let name = path.length > 0 ? JSON.stringify(path) : undefined;
actor.removeAll(host, name);
front.removeAll(host, name);
},
/**
* Handles removing all session cookies from the storage
*/
onRemoveAllSessionCookies: function () {
// Cannot use this.currentActor() if the handler is called from the
// tree context menu: it returns the correct value only after the
// table data from server is successfully fetched (and that's async).
let [, host, ...path] = this.tree.selectedItem;
let actor = this.getCurrentActor();
let front = this.getCurrentFront();
let name = path.length > 0 ? JSON.stringify(path) : undefined;
actor.removeAllSessionCookies(host, name);
front.removeAllSessionCookies(host, name);
},
/**
@ -1101,11 +1173,11 @@ StorageUI.prototype = {
*/
onRemoveAllFrom: function () {
let [, host] = this.tree.selectedItem;
let actor = this.getCurrentActor();
let front = this.getCurrentFront();
let rowId = this.table.contextMenuRowId;
let data = this.table.items.get(rowId);
actor.removeAll(host, data.host);
front.removeAll(host, data.host);
},
onRemoveTreeItem: function () {
@ -1119,9 +1191,9 @@ StorageUI.prototype = {
},
removeDatabase: function (host, dbName) {
let actor = this.storageTypes.indexedDB;
let front = this.getCurrentFront();
actor.removeDatabase(host, dbName).then(result => {
front.removeDatabase(host, dbName).then(result => {
if (result.blocked) {
let notificationBox = this._toolbox.getNotificationBox();
notificationBox.appendNotification(
@ -1141,8 +1213,17 @@ StorageUI.prototype = {
},
removeCache: function (host, cacheName) {
let actor = this.storageTypes.Cache;
let front = this.getCurrentFront();
actor.removeItem(host, JSON.stringify([ cacheName ]));
front.removeItem(host, JSON.stringify([ cacheName ]));
},
};
// Helper Functions
function createGUID() {
return "{cccccccc-cccc-4ccc-yccc-cccccccccccc}".replace(/[cy]/g, c => {
let r = Math.random() * 16 | 0, v = c == "c" ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}

View file

@ -32,6 +32,20 @@
min-width: 250px;
}
#storage-toolbar .add-button::before {
margin: 0;
background-image: url("chrome://devtools/skin/images/add.svg");
-moz-user-focus: normal;
}
#storage-toolbar .devtools-button {
min-width: unset;
}
#storage-toolbar .devtools-button hbox {
display: none;
}
/* Responsive sidebar */
@media (max-width: 700px) {
#storage-tree,

View file

@ -15,6 +15,8 @@ const {isWindowIncluded} = require("devtools/shared/layout/utils");
const specs = require("devtools/shared/specs/storage");
const { Task } = require("devtools/shared/task");
const DEFAULT_VALUE = "value";
// GUID to be used as a separator in compound keys. This must match the same
// constant in devtools/client/storage/ui.js,
// devtools/client/storage/test/head.js and
@ -616,6 +618,14 @@ StorageActors.createActor({
this.editCookie(data);
}),
addItem: Task.async(function* (guid) {
let doc = this.storageActor.document;
let time = new Date().getTime();
let expiry = new Date(time + 3600 * 24 * 1000).toGMTString();
doc.cookie = `${guid}=${DEFAULT_VALUE};expires=${expiry}`;
}),
removeItem: Task.async(function* (host, name) {
let doc = this.storageActor.document;
this.removeCookie(host, name, doc.nodePrincipal
@ -954,6 +964,12 @@ var cookieHelpers = {
let rowdata = msg.data.args[0];
return cookieHelpers.editCookie(rowdata);
}
case "createNewCookie": {
let host = msg.data.args[0];
let guid = msg.data.args[1];
let originAttributes = msg.data.args[2];
return cookieHelpers.createNewCookie(host, guid, originAttributes);
}
case "removeCookie": {
let host = msg.data.args[0];
let name = msg.data.args[1];
@ -1094,6 +1110,14 @@ function getObjectForLocalOrSessionStorage(type) {
];
}),
addItem: Task.async(function* (guid, host) {
let storage = this.hostVsStores.get(host);
if (!storage) {
return;
}
storage.setItem(guid, DEFAULT_VALUE);
}),
/**
* Edit localStorage or sessionStorage fields.
*

View file

@ -90,6 +90,13 @@ createStorageSpec({
methods: Object.assign({},
editRemoveMethods,
{
addItem: {
request: {
guid: Arg(0, "string"),
},
response: {}
}
}, {
removeAll: {
request: {
host: Arg(0, "string"),
@ -119,6 +126,14 @@ types.addDictType("storageobject", {
const storageMethods = Object.assign({},
editRemoveMethods,
{
addItem: {
request: {
guid: Arg(0, "string"),
host: Arg(1, "nullable:string")
},
response: {}
}
}, {
removeAll: {
request: {
host: Arg(0, "string")