summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--devtools/client/locales/en-US/netmonitor.properties5
-rw-r--r--devtools/client/locales/en-US/storage.dtd3
-rw-r--r--devtools/client/locales/en-US/storage.properties11
-rw-r--r--devtools/client/netmonitor/actions/index.js3
-rw-r--r--devtools/client/netmonitor/actions/moz.build1
-rw-r--r--devtools/client/netmonitor/actions/timing-markers.js19
-rw-r--r--devtools/client/netmonitor/components/summary-button.js24
-rw-r--r--devtools/client/netmonitor/constants.js2
-rw-r--r--devtools/client/netmonitor/netmonitor-controller.js2
-rw-r--r--devtools/client/netmonitor/reducers/index.js2
-rw-r--r--devtools/client/netmonitor/reducers/moz.build1
-rw-r--r--devtools/client/netmonitor/reducers/timing-markers.js52
-rw-r--r--devtools/client/netmonitor/requests-menu-view.js7
-rw-r--r--devtools/client/netmonitor/selectors/index.js18
-rw-r--r--devtools/client/shared/moz.build1
-rw-r--r--devtools/client/shared/natural-sort.js106
-rw-r--r--devtools/client/shared/widgets/TableWidget.js133
-rw-r--r--devtools/client/storage/storage.xul8
-rw-r--r--devtools/client/storage/test/browser.ini10
-rw-r--r--devtools/client/storage/test/browser_storage_basic.js52
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_add.js20
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_delete_all.js106
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_domain.js12
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_edit.js21
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js5
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_tab_navigation.js3
-rw-r--r--devtools/client/storage/test/browser_storage_delete.js10
-rw-r--r--devtools/client/storage/test/browser_storage_delete_all.js6
-rw-r--r--devtools/client/storage/test/browser_storage_delete_tree.js18
-rw-r--r--devtools/client/storage/test/browser_storage_dom_cache_disabled.js37
-rw-r--r--devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js188
-rw-r--r--devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js70
-rw-r--r--devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js83
-rw-r--r--devtools/client/storage/test/browser_storage_empty_objectstores.js10
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_delete.js6
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js10
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js31
-rw-r--r--devtools/client/storage/test/browser_storage_localstorage_add.js20
-rw-r--r--devtools/client/storage/test/browser_storage_overflow.js64
-rw-r--r--devtools/client/storage/test/browser_storage_sessionstorage_add.js20
-rw-r--r--devtools/client/storage/test/browser_storage_sidebar.js14
-rw-r--r--devtools/client/storage/test/browser_storage_sidebar_update.js2
-rw-r--r--devtools/client/storage/test/browser_storage_values.js18
-rw-r--r--devtools/client/storage/test/head.js123
-rw-r--r--devtools/client/storage/test/storage-indexeddb-duplicate-names.html50
-rw-r--r--devtools/client/storage/test/storage-listings.html16
-rw-r--r--devtools/client/storage/test/storage-overflow.html2
-rw-r--r--devtools/client/storage/test/storage-secured-iframe.html5
-rw-r--r--devtools/client/storage/test/storage-unsecured-iframe.html3
-rw-r--r--devtools/client/storage/test/storage-updates.html4
-rw-r--r--devtools/client/storage/ui.js300
-rw-r--r--devtools/client/themes/storage.css14
-rw-r--r--devtools/server/actors/storage.js470
-rw-r--r--devtools/server/tests/browser/browser.ini2
-rw-r--r--devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js105
-rw-r--r--devtools/server/tests/browser/browser_storage_dynamic_windows.js52
-rw-r--r--devtools/server/tests/browser/browser_storage_listings.js58
-rw-r--r--devtools/server/tests/browser/browser_storage_updates.js32
-rw-r--r--devtools/server/tests/browser/head.js36
-rw-r--r--devtools/server/tests/browser/storage-cookies-same-name.html28
-rw-r--r--devtools/shared/specs/storage.js28
-rw-r--r--toolkit/content/license.html31
62 files changed, 2159 insertions, 434 deletions
diff --git a/devtools/client/locales/en-US/netmonitor.properties b/devtools/client/locales/en-US/netmonitor.properties
index 021b56a2b..4926c234b 100644
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -150,6 +150,11 @@ networkMenu.empty=No requests
# #2 is the size, #3 is the transferred size, #4 is the number of seconds.
networkMenu.summary2=One request, #2 KB (transferred: #3 KB), #4 s;#1 requests, #2 KB (transferred: #3 KB), #4 s
+# LOCALIZATION NOTE (networkMenu.timeS): This is the label displayed
+# in the network table footer providing concise information about all requests.
+# Events DOMContentLoaded and load - specifying the number of seconds.
+networkMenu.timeS=%S s
+
# LOCALIZATION NOTE (networkMenu.sizeB): This is the label displayed
# in the network menu specifying the size of a request (in bytes).
networkMenu.sizeB=%S B
diff --git a/devtools/client/locales/en-US/storage.dtd b/devtools/client/locales/en-US/storage.dtd
index 211c79436..adfc03183 100644
--- a/devtools/client/locales/en-US/storage.dtd
+++ b/devtools/client/locales/en-US/storage.dtd
@@ -9,3 +9,6 @@
<!-- LOCALIZATION NOTE : Label of popup menu action to delete all storage items. -->
<!ENTITY storage.popupMenu.deleteAllLabel "Delete All">
+
+<!-- LOCALIZATION NOTE : Label of popup menu action to delete all session cookies. -->
+<!ENTITY storage.popupMenu.deleteAllSessionCookiesLabel "Delete All Session Cookies">
diff --git a/devtools/client/locales/en-US/storage.properties b/devtools/client/locales/en-US/storage.properties
index 1eeb88ff9..4719520bd 100644
--- a/devtools/client/locales/en-US/storage.properties
+++ b/devtools/client/locales/en-US/storage.properties
@@ -35,6 +35,7 @@ tree.labels.Cache=Cache Storage
# LOCALIZATION NOTE (table.headers.*.*):
# These strings are the header names of the columns in the Storage Table for
# each type of storage available through the Storage Tree to the side.
+table.headers.cookies.uniqueKey=Unique key
table.headers.cookies.name=Name
table.headers.cookies.path=Path
table.headers.cookies.host=Domain
@@ -52,14 +53,16 @@ table.headers.sessionStorage.value=Value
table.headers.Cache.url=URL
table.headers.Cache.status=Status
+table.headers.indexedDB.uniqueKey=Unique key
table.headers.indexedDB.name=Key
table.headers.indexedDB.db=Database Name
+table.headers.indexedDB.storage=Storage
table.headers.indexedDB.objectStore=Object Store Name
table.headers.indexedDB.value=Value
table.headers.indexedDB.origin=Origin
table.headers.indexedDB.version=Version
table.headers.indexedDB.objectStores=Object Stores
-table.headers.indexedDB.keyPath=Key
+table.headers.indexedDB.keyPath2=Key Path
table.headers.indexedDB.autoIncrement=Auto Increment
table.headers.indexedDB.indexes=Indexes
@@ -84,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”
diff --git a/devtools/client/netmonitor/actions/index.js b/devtools/client/netmonitor/actions/index.js
index 2ce23448e..de8f4ae1d 100644
--- a/devtools/client/netmonitor/actions/index.js
+++ b/devtools/client/netmonitor/actions/index.js
@@ -5,6 +5,7 @@
const filters = require("./filters");
const requests = require("./requests");
+const timingMarkers = require("./timing-markers");
const ui = require("./ui");
-module.exports = Object.assign({}, filters, requests, ui);
+module.exports = Object.assign({}, filters, requests, timingMarkers, ui);
diff --git a/devtools/client/netmonitor/actions/moz.build b/devtools/client/netmonitor/actions/moz.build
index d0ac61944..ce904cae8 100644
--- a/devtools/client/netmonitor/actions/moz.build
+++ b/devtools/client/netmonitor/actions/moz.build
@@ -7,5 +7,6 @@ DevToolsModules(
'filters.js',
'index.js',
'requests.js',
+ 'timing-markers.js',
'ui.js',
)
diff --git a/devtools/client/netmonitor/actions/timing-markers.js b/devtools/client/netmonitor/actions/timing-markers.js
new file mode 100644
index 000000000..4f1363a70
--- /dev/null
+++ b/devtools/client/netmonitor/actions/timing-markers.js
@@ -0,0 +1,19 @@
+/* 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";
+
+const { ADD_TIMING_MARKER, CLEAR_TIMING_MARKERS } = require("../constants");
+
+exports.addTimingMarker = (marker) => {
+ return {
+ type: ADD_TIMING_MARKER,
+ marker
+ };
+};
+
+exports.clearTimingMarkers = () => {
+ return {
+ type: CLEAR_TIMING_MARKERS
+ };
+};
diff --git a/devtools/client/netmonitor/components/summary-button.js b/devtools/client/netmonitor/components/summary-button.js
index 223552fbf..00595e5e6 100644
--- a/devtools/client/netmonitor/components/summary-button.js
+++ b/devtools/client/netmonitor/components/summary-button.js
@@ -15,16 +15,22 @@ const { connect } = require("devtools/client/shared/vendor/react-redux");
const { PluralForm } = require("devtools/shared/plural-form");
const { L10N } = require("../l10n");
const {
- getDisplayedRequestsSummary
+ getDisplayedRequestsSummary,
+ getDisplayedTimingMarker
} = require("../selectors/index");
const { button, span } = DOM;
function SummaryButton({
summary,
- triggerSummary
+ triggerSummary,
+ timingMarkers
}) {
let { count, contentSize, transferredSize, millis } = summary;
+ let {
+ DOMContentLoaded,
+ load,
+ } = timingMarkers;
const text = (count === 0) ? L10N.getStr("networkMenu.empty") :
PluralForm.get(count, L10N.getStr("networkMenu.summary2"))
.replace("#1", count)
@@ -33,7 +39,13 @@ function SummaryButton({
.replace("#3", L10N.numberWithDecimals(transferredSize / 1024,
CONTENT_SIZE_DECIMALS))
.replace("#4", L10N.numberWithDecimals(millis / 1000,
- REQUEST_TIME_DECIMALS));
+ REQUEST_TIME_DECIMALS))
+ + ((DOMContentLoaded > -1)
+ ? ", " + "DOMContentLoaded: " + L10N.getFormatStrWithNumbers("networkMenu.timeS", L10N.numberWithDecimals(DOMContentLoaded / 1000, REQUEST_TIME_DECIMALS))
+ : "")
+ + ((load > -1)
+ ? ", " + "load: " + L10N.getFormatStrWithNumbers("networkMenu.timeS", L10N.numberWithDecimals(load / 1000, REQUEST_TIME_DECIMALS))
+ : "");
return button({
id: "requests-menu-network-summary-button",
@@ -47,11 +59,17 @@ function SummaryButton({
SummaryButton.propTypes = {
summary: PropTypes.object.isRequired,
+ timingMarkers: PropTypes.object.isRequired,
};
module.exports = connect(
(state) => ({
summary: getDisplayedRequestsSummary(state),
+ timingMarkers: {
+ DOMContentLoaded:
+ getDisplayedTimingMarker(state, "firstDocumentDOMContentLoadedTimestamp"),
+ load: getDisplayedTimingMarker(state, "firstDocumentLoadTimestamp"),
+ },
}),
(dispatch) => ({
triggerSummary: () => {
diff --git a/devtools/client/netmonitor/constants.js b/devtools/client/netmonitor/constants.js
index 33bc71ea7..1605496a5 100644
--- a/devtools/client/netmonitor/constants.js
+++ b/devtools/client/netmonitor/constants.js
@@ -10,6 +10,8 @@ const general = {
};
const actionTypes = {
+ ADD_TIMING_MARKER: "ADD_TIMING_MARKER",
+ CLEAR_TIMING_MARKERS: "CLEAR_TIMING_MARKERS",
TOGGLE_FILTER_TYPE: "TOGGLE_FILTER_TYPE",
ENABLE_FILTER_TYPE_ONLY: "ENABLE_FILTER_TYPE_ONLY",
SET_FILTER_TEXT: "SET_FILTER_TEXT",
diff --git a/devtools/client/netmonitor/netmonitor-controller.js b/devtools/client/netmonitor/netmonitor-controller.js
index 739e174fb..939b6e4f5 100644
--- a/devtools/client/netmonitor/netmonitor-controller.js
+++ b/devtools/client/netmonitor/netmonitor-controller.js
@@ -414,6 +414,7 @@ TargetEventsHandler.prototype = {
}
// Clear any accumulated markers.
NetMonitorController.NetworkEventsHandler.clearMarkers();
+ gStore.dispatch(Actions.clearTimingMarkers());
window.emit(EVENTS.TARGET_WILL_NAVIGATE);
break;
@@ -534,6 +535,7 @@ NetworkEventsHandler.prototype = {
_onDocLoadingMarker: function (marker) {
window.emit(EVENTS.TIMELINE_EVENT, marker);
this._markers.push(marker);
+ gStore.dispatch(Actions.addTimingMarker(marker));
},
/**
diff --git a/devtools/client/netmonitor/reducers/index.js b/devtools/client/netmonitor/reducers/index.js
index f36b1d91f..f003c7805 100644
--- a/devtools/client/netmonitor/reducers/index.js
+++ b/devtools/client/netmonitor/reducers/index.js
@@ -6,10 +6,12 @@
const { combineReducers } = require("devtools/client/shared/vendor/redux");
const filters = require("./filters");
const requests = require("./requests");
+const timingMarkers = require("./timing-markers");
const ui = require("./ui");
module.exports = combineReducers({
filters,
requests,
+ timingMarkers,
ui,
});
diff --git a/devtools/client/netmonitor/reducers/moz.build b/devtools/client/netmonitor/reducers/moz.build
index d0ac61944..ce904cae8 100644
--- a/devtools/client/netmonitor/reducers/moz.build
+++ b/devtools/client/netmonitor/reducers/moz.build
@@ -7,5 +7,6 @@ DevToolsModules(
'filters.js',
'index.js',
'requests.js',
+ 'timing-markers.js',
'ui.js',
)
diff --git a/devtools/client/netmonitor/reducers/timing-markers.js b/devtools/client/netmonitor/reducers/timing-markers.js
new file mode 100644
index 000000000..cb500b2c4
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/timing-markers.js
@@ -0,0 +1,52 @@
+/* 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";
+
+const I = require("devtools/client/shared/vendor/immutable");
+const { ADD_TIMING_MARKER,
+ CLEAR_TIMING_MARKERS } = require("../constants");
+
+const TimingMarkers = I.Record({
+ firstDocumentDOMContentLoadedTimestamp: -1,
+ firstDocumentLoadTimestamp: -1,
+});
+
+function addTimingMarker(state, action) {
+ if (action.marker.name == "document::DOMContentLoaded" &&
+ state.firstDocumentDOMContentLoadedTimestamp == -1) {
+ return state.set("firstDocumentDOMContentLoadedTimestamp",
+ action.marker.unixTime / 1000);
+ }
+
+ if (action.marker.name == "document::Load" &&
+ state.firstDocumentLoadTimestamp == -1) {
+ return state.set("firstDocumentLoadTimestamp",
+ action.marker.unixTime / 1000);
+ }
+
+ return state;
+}
+
+function clearTimingMarkers(state) {
+ return state.withMutations(st => {
+ st.remove("firstDocumentDOMContentLoadedTimestamp");
+ st.remove("firstDocumentLoadTimestamp");
+ });
+}
+
+function timingMarkers(state = new TimingMarkers(), action) {
+ switch (action.type) {
+ case ADD_TIMING_MARKER:
+ return addTimingMarker(state, action);
+
+ case CLEAR_TIMING_MARKERS:
+ return clearTimingMarkers(state);
+
+ default:
+ return state;
+ }
+}
+
+module.exports = timingMarkers;
diff --git a/devtools/client/netmonitor/requests-menu-view.js b/devtools/client/netmonitor/requests-menu-view.js
index 4ee307145..c491fcb0e 100644
--- a/devtools/client/netmonitor/requests-menu-view.js
+++ b/devtools/client/netmonitor/requests-menu-view.js
@@ -275,6 +275,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
this._addQueue = [];
this._updateQueue = [];
this._firstRequestStartedMillis = -1;
+ this._firstRequestStartedMillisInRequests = false;
this._lastRequestEndedMillis = -1;
},
@@ -650,6 +651,9 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
// Append a network request item to this container.
let requestItem = this.push([menuView, id], {
attachment: {
+ firstRequestStartedMillis: this._firstRequestStartedMillisInRequests
+ ? null
+ : this._firstRequestStartedMillis,
startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
startedMillis: unixTime,
method: method,
@@ -661,6 +665,8 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
}
});
+ this._firstRequestStartedMillisInRequests = true;
+
if (id == this._preferredItemId) {
this.selectedItem = requestItem;
}
@@ -1563,6 +1569,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
_ctx: null,
_cachedWaterfallWidth: 0,
_firstRequestStartedMillis: -1,
+ _firstRequestStartedMillisInRequests: false,
_lastRequestEndedMillis: -1,
_updateQueue: [],
_addQueue: [],
diff --git a/devtools/client/netmonitor/selectors/index.js b/devtools/client/netmonitor/selectors/index.js
index 60d6007cd..612188758 100644
--- a/devtools/client/netmonitor/selectors/index.js
+++ b/devtools/client/netmonitor/selectors/index.js
@@ -73,6 +73,24 @@ const getDisplayedRequestsSummary = createSelector(
})
);
+function getDisplayedTimingMarker(state, marker) {
+ let timingMarker = null;
+ if (state.timingMarkers) {
+ timingMarker = state.timingMarkers.get(marker);
+ }
+ let firstRequestStartedMillis = null;
+ if (state.requests.items.length) {
+ firstRequestStartedMillis = state.requests.items[0]
+ .attachment.firstRequestStartedMillis;
+ }
+ if (timingMarker && firstRequestStartedMillis) {
+ return timingMarker - firstRequestStartedMillis;
+ } else {
+ return -1;
+ }
+}
+
module.exports = {
getDisplayedRequestsSummary,
+ getDisplayedTimingMarker,
};
diff --git a/devtools/client/shared/moz.build b/devtools/client/shared/moz.build
index 1c61970c0..7be4a0088 100644
--- a/devtools/client/shared/moz.build
+++ b/devtools/client/shared/moz.build
@@ -35,6 +35,7 @@ DevToolsModules(
'Jsbeautify.jsm',
'key-shortcuts.js',
'keycodes.js',
+ 'natural-sort.js',
'network-throttling-profiles.js',
'node-attribute-parser.js',
'options-view.js',
diff --git a/devtools/client/shared/natural-sort.js b/devtools/client/shared/natural-sort.js
new file mode 100644
index 000000000..904d76431
--- /dev/null
+++ b/devtools/client/shared/natural-sort.js
@@ -0,0 +1,106 @@
+/*
+ * Natural Sort algorithm for Javascript - Version 0.8.1 - Released under MIT license
+ * Author: Jim Palmer (based on chunking idea from Dave Koelle)
+ *
+ * Includes pull request to move regexes out of main function for performance
+ * increases.
+ *
+ * Repository:
+ * https://github.com/overset/javascript-natural-sort/
+ */
+
+"use strict";
+
+var re = /(^([+\-]?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(?=\D|\s|$))|^0x[\da-fA-F]+$|\d+)/g;
+var sre = /^\s+|\s+$/g; // trim pre-post whitespace
+var snre = /\s+/g; // normalize all whitespace to single ' ' character
+
+// eslint-disable-next-line
+var dre = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/;
+var hre = /^0x[0-9a-f]+$/i;
+var ore = /^0/;
+var b0re = /^\0/;
+var e0re = /\0$/;
+
+exports.naturalSortCaseSensitive =
+function naturalSortCaseSensitive(a, b) {
+ return naturalSort(a, b, false);
+};
+
+exports.naturalSortCaseInsensitive =
+function naturalSortCaseInsensitive(a, b) {
+ return naturalSort(a, b, true);
+};
+
+/**
+ * Sort numbers, strings, IP Addresses, Dates, Filenames, version numbers etc.
+ * "the way humans do."
+ *
+ * This function should only be called via naturalSortCaseSensitive and
+ * naturalSortCaseInsensitive.
+ *
+ * e.g. [3, 2, 1, 10].sort(naturalSort)
+ *
+ * @param {Object} a
+ * Passed in by Array.sort(a, b)
+ * @param {Object} b
+ * Passed in by Array.sort(a, b)
+ * @param {Boolean} insensitive
+ * Should the search be case insensitive?
+ */
+function naturalSort(a, b, insensitive) {
+ // convert all to strings strip whitespace
+ let i = function (s) {
+ return (insensitive && ("" + s).toLowerCase() || "" + s)
+ .replace(sre, "");
+ };
+ let x = i(a) || "";
+ let y = i(b) || "";
+ // chunk/tokenize
+ let xN = x.replace(re, "\0$1\0").replace(e0re, "").replace(b0re, "").split("\0");
+ let yN = y.replace(re, "\0$1\0").replace(e0re, "").replace(b0re, "").split("\0");
+ // numeric, hex or date detection
+ let xD = parseInt(x.match(hre), 16) || (xN.length !== 1 && Date.parse(x));
+ let yD = parseInt(y.match(hre), 16) || xD && y.match(dre) && Date.parse(y) || null;
+ let normChunk = function (s, l) {
+ // normalize spaces; find floats not starting with '0', string or 0 if
+ // not defined (Clint Priest)
+ return (!s.match(ore) || l == 1) &&
+ parseFloat(s) || s.replace(snre, " ").replace(sre, "") || 0;
+ };
+ let oFxNcL;
+ let oFyNcL;
+
+ // first try and sort Hex codes or Dates
+ if (yD) {
+ if (xD < yD) {
+ return -1;
+ } else if (xD > yD) {
+ return 1;
+ }
+ }
+
+ // natural sorting through split numeric strings and default strings
+ // eslint-disable-next-line
+ for (let cLoc = 0, xNl = xN.length, yNl = yN.length, numS = Math.max(xNl, yNl); cLoc < numS; cLoc++) {
+ oFxNcL = normChunk(xN[cLoc] || "", xNl);
+ oFyNcL = normChunk(yN[cLoc] || "", yNl);
+
+ // handle numeric vs string comparison - number < string - (Kyle Adams)
+ if (isNaN(oFxNcL) !== isNaN(oFyNcL)) {
+ return isNaN(oFxNcL) ? 1 : -1;
+ }
+ // if unicode use locale comparison
+ // eslint-disable-next-line
+ if (/[^\x00-\x80]/.test(oFxNcL + oFyNcL) && oFxNcL.localeCompare) {
+ let comp = oFxNcL.localeCompare(oFyNcL);
+ return comp / Math.abs(comp);
+ }
+ if (oFxNcL < oFyNcL) {
+ return -1;
+ } else if (oFxNcL > oFyNcL) {
+ return 1;
+ }
+ }
+ return null;
+}
diff --git a/devtools/client/shared/widgets/TableWidget.js b/devtools/client/shared/widgets/TableWidget.js
index 5dacd1b67..a0f0dfc11 100644
--- a/devtools/client/shared/widgets/TableWidget.js
+++ b/devtools/client/shared/widgets/TableWidget.js
@@ -8,6 +8,8 @@ loader.lazyRequireGetter(this, "setNamedTimeout",
"devtools/client/shared/widgets/view-helpers", true);
loader.lazyRequireGetter(this, "clearNamedTimeout",
"devtools/client/shared/widgets/view-helpers", true);
+loader.lazyRequireGetter(this, "naturalSortCaseInsensitive",
+ "devtools/client/shared/natural-sort", true);
const {KeyCodes} = require("devtools/client/shared/keycodes");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@@ -123,6 +125,8 @@ function TableWidget(node, options = {}) {
TableWidget.prototype = {
items: null,
+ editBookmark: null,
+ scrollIntoViewOnUpdate: null,
/**
* Getter for the headers context menu popup id.
@@ -139,7 +143,12 @@ TableWidget.prototype = {
*/
set selectedRow(id) {
for (let column of this.columns.values()) {
- column.selectRow(id[this.uniqueId] || id);
+ if (id) {
+ column.selectRow(id[this.uniqueId] || id);
+ } else {
+ column.selectedRow = null;
+ column.selectRow(null);
+ }
}
},
@@ -615,8 +624,13 @@ TableWidget.prototype = {
/**
* Populates the header context menu with the names of the columns along with
* displaying which columns are hidden or visible.
+ *
+ * @param {Array} privateColumns=[]
+ * An array of column names that should never appear in the table. This
+ * allows us to e.g. have an invisible compound primary key for a
+ * table's rows.
*/
- populateMenuPopup: function () {
+ populateMenuPopup: function (privateColumns = []) {
if (!this.menupopup) {
return;
}
@@ -626,6 +640,10 @@ TableWidget.prototype = {
}
for (let column of this.columns.values()) {
+ if (privateColumns.includes(column.id)) {
+ continue;
+ }
+
let menuitem = this.document.createElementNS(XUL_NS, "menuitem");
menuitem.setAttribute("label", column.header.getAttribute("value"));
menuitem.setAttribute("data-id", column.id);
@@ -663,16 +681,21 @@ TableWidget.prototype = {
* Creates the columns in the table. Without calling this method, data cannot
* be inserted into the table unless `initialColumns` was supplied.
*
- * @param {object} columns
+ * @param {Object} columns
* A key value pair representing the columns of the table. Where the
* key represents the id of the column and the value is the displayed
* label in the header of the column.
- * @param {string} sortOn
+ * @param {String} sortOn
* The id of the column on which the table will be initially sorted on.
- * @param {array} hiddenColumns
+ * @param {Array} hiddenColumns
* Ids of all the columns that are hidden by default.
+ * @param {Array} privateColumns=[]
+ * An array of column names that should never appear in the table. This
+ * allows us to e.g. have an invisible compound primary key for a
+ * table's rows.
*/
- setColumns: function (columns, sortOn = this.sortedOn, hiddenColumns = []) {
+ setColumns: function (columns, sortOn = this.sortedOn, hiddenColumns = [],
+ privateColumns = []) {
for (let column of this.columns.values()) {
column.destroy();
}
@@ -702,13 +725,18 @@ TableWidget.prototype = {
}
this.columns.set(id, new Column(this, id, columns[id]));
- if (hiddenColumns.indexOf(id) > -1) {
+ if (hiddenColumns.includes(id) || privateColumns.includes(id)) {
+ // Hide the column.
this.columns.get(id).toggleColumn();
+
+ if (privateColumns.includes(id)) {
+ this.columns.get(id).private = true;
+ }
}
}
this.sortedOn = sortOn;
this.sortBy(this.sortedOn);
- this.populateMenuPopup();
+ this.populateMenuPopup(privateColumns);
},
/**
@@ -778,6 +806,11 @@ TableWidget.prototype = {
return;
}
+ if (this.editBookmark && !this.items.has(this.editBookmark)) {
+ // Key has been updated... update bookmark.
+ this.editBookmark = item[this.uniqueId];
+ }
+
let index = this.columns.get(this.sortedOn).push(item);
for (let [key, column] of this.columns) {
if (key != this.sortedOn) {
@@ -814,7 +847,8 @@ TableWidget.prototype = {
column.remove(item);
column.updateZebra();
}
- if (this.items.size == 0) {
+ if (this.items.size === 0) {
+ this.selectedRow = null;
this.tbody.setAttribute("empty", "empty");
}
@@ -857,6 +891,8 @@ TableWidget.prototype = {
this.tbody.setAttribute("empty", "empty");
this.setPlaceholderText(this.emptyText);
+ this.selectedRow = null;
+
this.emit(EVENTS.TABLE_CLEARED, this);
},
@@ -958,6 +994,9 @@ module.exports.TableWidget = TableWidget;
* The displayed string on the column's header.
*/
function Column(table, id, header) {
+ // By default cells are visible in the UI.
+ this._private = false;
+
this.tbody = table.tbody;
this.document = table.document;
this.window = table.window;
@@ -1041,6 +1080,23 @@ Column.prototype = {
},
/**
+ * Get the private state of the column (visibility in the UI).
+ */
+ get private() {
+ return this._private;
+ },
+
+ /**
+ * Set the private state of the column (visibility in the UI).
+ *
+ * @param {Boolean} state
+ * Private (true or false)
+ */
+ set private(state) {
+ this._private = state;
+ },
+
+ /**
* Sets the sorted value
*/
set sorted(value) {
@@ -1115,7 +1171,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
@@ -1124,7 +1182,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
@@ -1136,6 +1210,7 @@ Column.prototype = {
this.cells[this.items[id]].flash();
}
+
this.updateZebra();
},
@@ -1160,15 +1235,16 @@ Column.prototype = {
*/
selectRowAt: function (index) {
if (this.selectedRow != null) {
- this.cells[this.items[this.selectedRow]].toggleClass("theme-selected");
+ this.cells[this.items[this.selectedRow]].classList.remove("theme-selected");
}
- if (index < 0) {
+
+ let cell = this.cells[index];
+ if (cell) {
+ cell.classList.add("theme-selected");
+ this.selectedRow = cell.id;
+ } else {
this.selectedRow = null;
- return;
}
- let cell = this.cells[index];
- cell.toggleClass("theme-selected");
- this.selectedRow = cell.id;
},
/**
@@ -1218,11 +1294,11 @@ Column.prototype = {
let index;
if (this.sorted == 1) {
index = this.cells.findIndex(element => {
- return value < element.value;
+ return naturalSortCaseInsensitive(value, element.value) === -1;
});
} else {
index = this.cells.findIndex(element => {
- return value > element.value;
+ return naturalSortCaseInsensitive(value, element.value) === 1;
});
}
index = index >= 0 ? index : this.cells.length;
@@ -1332,7 +1408,6 @@ Column.prototype = {
this.cells = [];
this.items = {};
this._itemsDirty = false;
- this.selectedRow = null;
while (this.header.nextSibling) {
this.header.nextSibling.remove();
}
@@ -1350,7 +1425,7 @@ Column.prototype = {
a[this.id].textContent : a[this.id];
let val2 = (b[this.id] instanceof Node) ?
b[this.id].textContent : b[this.id];
- return val1 > val2;
+ return naturalSortCaseInsensitive(val1, val2);
});
} else if (this.sorted > 1) {
items.sort((a, b) => {
@@ -1358,12 +1433,12 @@ Column.prototype = {
a[this.id].textContent : a[this.id];
let val2 = (b[this.id] instanceof Node) ?
b[this.id].textContent : b[this.id];
- return val2 > val1;
+ return naturalSortCaseInsensitive(val2, val1);
});
}
if (this.selectedRow) {
- this.cells[this.items[this.selectedRow]].toggleClass("theme-selected");
+ this.cells[this.items[this.selectedRow]].classList.remove("theme-selected");
}
this.items = {};
// Otherwise, just use the sorted array passed to update the cells value.
@@ -1373,7 +1448,7 @@ Column.prototype = {
this.cells[i].id = item[this.uniqueId];
});
if (this.selectedRow) {
- this.cells[this.items[this.selectedRow]].toggleClass("theme-selected");
+ this.cells[this.items[this.selectedRow]].classList.add("theme-selected");
}
this._itemsDirty = false;
this.updateZebra();
@@ -1387,7 +1462,9 @@ Column.prototype = {
if (!cell.hidden) {
i++;
}
- cell.toggleClass("even", !(i % 2));
+
+ let even = !(i % 2);
+ cell.classList.toggle("even", even);
}
},
@@ -1523,8 +1600,8 @@ Cell.prototype = {
return this._value;
},
- toggleClass: function (className, condition) {
- this.label.classList.toggle(className, condition);
+ get classList() {
+ return this.label.classList;
},
/**
@@ -1550,6 +1627,10 @@ Cell.prototype = {
this.label.focus();
},
+ scrollIntoView: function () {
+ this.label.scrollIntoView(false);
+ },
+
destroy: function () {
this.label.remove();
this.label = null;
diff --git a/devtools/client/storage/storage.xul b/devtools/client/storage/storage.xul
index 9fbef5199..a91900add 100644
--- a/devtools/client/storage/storage.xul
+++ b/devtools/client/storage/storage.xul
@@ -26,13 +26,18 @@
<menupopup id="storage-tree-popup">
<menuitem id="storage-tree-popup-delete-all"
label="&storage.popupMenu.deleteAllLabel;"/>
+ <menuitem id="storage-tree-popup-delete-all-session-cookies"
+ label="&storage.popupMenu.deleteAllSessionCookiesLabel;"/>
<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"
label="&storage.popupMenu.deleteAllLabel;"/>
+ <menuitem id="storage-table-popup-delete-all-session-cookies"
+ label="&storage.popupMenu.deleteAllSessionCookiesLabel;"/>
</menupopup>
</popupset>
@@ -41,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"
diff --git a/devtools/client/storage/test/browser.ini b/devtools/client/storage/test/browser.ini
index dd7f48bd7..1250d35d5 100644
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -7,6 +7,7 @@ support-files =
storage-cookies.html
storage-empty-objectstores.html
storage-idb-delete-blocked.html
+ storage-indexeddb-duplicate-names.html
storage-listings.html
storage-localstorage.html
storage-overflow.html
@@ -21,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]
@@ -29,15 +31,21 @@ support-files =
[browser_storage_delete.js]
[browser_storage_delete_all.js]
[browser_storage_delete_tree.js]
-[browser_storage_dynamic_updates.js]
+[browser_storage_dom_cache_disabled.js]
+[browser_storage_dynamic_updates_cookies.js]
+[browser_storage_dynamic_updates_localStorage.js]
+[browser_storage_dynamic_updates_sessionStorage.js]
[browser_storage_empty_objectstores.js]
[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]
diff --git a/devtools/client/storage/test/browser_storage_basic.js b/devtools/client/storage/test/browser_storage_basic.js
index 343d46170..7585eed1f 100644
--- a/devtools/client/storage/test/browser_storage_basic.js
+++ b/devtools/client/storage/test/browser_storage_basic.js
@@ -2,6 +2,8 @@
* 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 */
+
// Basic test to assert that the storage tree and table corresponding to each
// item in the storage tree is correctly displayed
@@ -21,10 +23,30 @@
"use strict";
const testCases = [
- [["cookies", "test1.example.org"],
- ["c1", "cs2", "c3", "uc1"]],
- [["cookies", "sectest1.example.org"],
- ["uc1", "cs2", "sc1"]],
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c3", "test1.example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/")
+ ]
+ ],
+ [
+ ["cookies", "sectest1.example.org"],
+ [
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("sc1", "sectest1.example.org",
+ "/browser/devtools/client/storage/test/"),
+ getCookieId("sc2", "sectest1.example.org",
+ "/browser/devtools/client/storage/test/")
+ ]
+ ],
[["localStorage", "http://test1.example.org"],
["ls1", "ls2"]],
[["localStorage", "http://sectest1.example.org"],
@@ -38,28 +60,28 @@ const testCases = [
[["sessionStorage", "https://sectest1.example.org"],
["iframe-s-ss1"]],
[["indexedDB", "http://test1.example.org"],
- ["idb1", "idb2"]],
- [["indexedDB", "http://test1.example.org", "idb1"],
+ ["idb1 (default)", "idb2 (default)"]],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)"],
["obj1", "obj2"]],
- [["indexedDB", "http://test1.example.org", "idb2"],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)"],
["obj3"]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
[1, 2, 3]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj2"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"],
[1]],
- [["indexedDB", "http://test1.example.org", "idb2", "obj3"],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)", "obj3"],
[]],
[["indexedDB", "http://sectest1.example.org"],
[]],
[["indexedDB", "https://sectest1.example.org"],
- ["idb-s1", "idb-s2"]],
- [["indexedDB", "https://sectest1.example.org", "idb-s1"],
+ ["idb-s1 (default)", "idb-s2 (default)"]],
+ [["indexedDB", "https://sectest1.example.org", "idb-s1 (default)"],
["obj-s1"]],
- [["indexedDB", "https://sectest1.example.org", "idb-s2"],
+ [["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"],
["obj-s2"]],
- [["indexedDB", "https://sectest1.example.org", "idb-s1", "obj-s1"],
+ [["indexedDB", "https://sectest1.example.org", "idb-s1 (default)", "obj-s1"],
[6, 7]],
- [["indexedDB", "https://sectest1.example.org", "idb-s2", "obj-s2"],
+ [["indexedDB", "https://sectest1.example.org", "idb-s2 (default)", "obj-s2"],
[16]],
[["Cache", "http://test1.example.org", "plop"],
[MAIN_DOMAIN + "404_cached_file.js",
diff --git a/devtools/client/storage/test/browser_storage_cookies_add.js b/devtools/client/storage/test/browser_storage_cookies_add.js
new file mode 100644
index 000000000..ac66eb92c
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_add.js
@@ -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();
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_delete_all.js b/devtools/client/storage/test/browser_storage_cookies_delete_all.js
index 6e6008e66..4640ebecb 100644
--- a/devtools/client/storage/test/browser_storage_cookies_delete_all.js
+++ b/devtools/client/storage/test/browser_storage_cookies_delete_all.js
@@ -8,11 +8,13 @@
// Test deleting all cookies
-function* performDelete(store, rowName, deleteAll) {
+function* performDelete(store, rowName, action) {
let contextMenu = gPanelWindow.document.getElementById(
"storage-table-popup");
let menuDeleteAllItem = contextMenu.querySelector(
"#storage-table-popup-delete-all");
+ let menuDeleteAllSessionCookiesItem = contextMenu.querySelector(
+ "#storage-table-popup-delete-all-session-cookies");
let menuDeleteAllFromItem = contextMenu.querySelector(
"#storage-table-popup-delete-all-from");
@@ -21,17 +23,23 @@ function* performDelete(store, rowName, deleteAll) {
yield selectTreeItem(store);
let eventWait = gUI.once("store-objects-updated");
+ let cells = getRowCells(rowName, true);
- let cells = getRowCells(rowName);
yield waitForContextMenu(contextMenu, cells.name, () => {
info(`Opened context menu in ${storeName}, row '${rowName}'`);
- if (deleteAll) {
- menuDeleteAllItem.click();
- } else {
- menuDeleteAllFromItem.click();
- let hostName = cells.host.value;
- ok(menuDeleteAllFromItem.getAttribute("label").includes(hostName),
+ switch (action) {
+ case "deleteAll":
+ menuDeleteAllItem.click();
+ break;
+ case "deleteAllSessionCookies":
+ menuDeleteAllSessionCookiesItem.click();
+ break;
+ case "deleteAllFrom":
+ menuDeleteAllFromItem.click();
+ let hostName = cells.host.value;
+ ok(menuDeleteAllFromItem.getAttribute("label").includes(hostName),
`Context menu item label contains '${hostName}'`);
+ break;
}
});
@@ -43,24 +51,94 @@ add_task(function* () {
info("test state before delete");
yield checkState([
- [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]],
- [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]],
+ [
+ ["cookies", "test1.example.org"], [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("c3", "test1.example.org", "/"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/")
+ ]
+ ],
+ [
+ ["cookies", "sectest1.example.org"], [
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("sc1", "sectest1.example.org",
+ "/browser/devtools/client/storage/test/"),
+ getCookieId("sc2", "sectest1.example.org",
+ "/browser/devtools/client/storage/test/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/")
+ ]
+ ],
]);
info("delete all from domain");
// delete only cookies that match the host exactly
- yield performDelete(["cookies", "test1.example.org"], "c1", false);
+ let id = getCookieId("c1", "test1.example.org", "/browser");
+ yield performDelete(["cookies", "test1.example.org"], id, "deleteAllFrom");
+ yield performDelete(["cookies", "test1.example.org"], id, false);
info("test state after delete all from domain");
yield checkState([
// Domain cookies (.example.org) must not be deleted.
- [["cookies", "test1.example.org"], ["cs2", "uc1"]],
- [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]],
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/")
+ ]
+ ],
+ [
+ ["cookies", "sectest1.example.org"],
+ [
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ getCookieId("sc1", "sectest1.example.org",
+ "/browser/devtools/client/storage/test/"),
+ getCookieId("sc2", "sectest1.example.org",
+ "/browser/devtools/client/storage/test/")
+ ]
+ ],
+ ]);
+
+ info("delete all session cookies");
+ // delete only session cookies
+ id = getCookieId("cs2", ".example.org", "/");
+ yield performDelete(["cookies", "sectest1.example.org"], id,
+ "deleteAllSessionCookies");
+
+ info("test state after delete all session cookies");
+ yield checkState([
+ // Cookies with expiry date must not be deleted.
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/")
+ ]
+ ],
+ [
+ ["cookies", "sectest1.example.org"],
+ [
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ getCookieId("sc2", "sectest1.example.org",
+ "/browser/devtools/client/storage/test/")
+ ]
+ ],
]);
info("delete all");
// delete all cookies for host, including domain cookies
- yield performDelete(["cookies", "sectest1.example.org"], "uc1", true);
+ id = getCookieId("uc2", ".example.org", "/");
+ yield performDelete(["cookies", "sectest1.example.org"], id, "deleteAll");
info("test state after delete all");
yield checkState([
diff --git a/devtools/client/storage/test/browser_storage_cookies_domain.js b/devtools/client/storage/test/browser_storage_cookies_domain.js
index dc93d6e67..7b194b73e 100644
--- a/devtools/client/storage/test/browser_storage_cookies_domain.js
+++ b/devtools/client/storage/test/browser_storage_cookies_domain.js
@@ -13,8 +13,16 @@ add_task(function* () {
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
yield checkState([
- [["cookies", "test1.example.org"],
- ["test1", "test2", "test3", "test4", "test5"]],
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("test1", ".test1.example.org", "/browser"),
+ getCookieId("test2", "test1.example.org", "/browser"),
+ getCookieId("test3", ".test1.example.org", "/browser"),
+ getCookieId("test4", "test1.example.org", "/browser"),
+ getCookieId("test5", ".test1.example.org", "/browser")
+ ]
+ ],
]);
yield finishTests();
diff --git a/devtools/client/storage/test/browser_storage_cookies_edit.js b/devtools/client/storage/test/browser_storage_cookies_edit.js
index 5818e4864..14944b398 100644
--- a/devtools/client/storage/test/browser_storage_cookies_edit.js
+++ b/devtools/client/storage/test/browser_storage_cookies_edit.js
@@ -10,13 +10,20 @@ add_task(function* () {
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
showAllColumns(true);
- yield editCell("test3", "name", "newTest3");
- yield editCell("newTest3", "path", "/");
- yield editCell("newTest3", "host", "test1.example.org");
- yield editCell("newTest3", "expires", "Tue, 14 Feb 2040 17:41:14 GMT");
- yield editCell("newTest3", "value", "newValue3");
- yield editCell("newTest3", "isSecure", "true");
- yield editCell("newTest3", "isHttpOnly", "true");
+ let id = getCookieId("test3", ".test1.example.org", "/browser");
+ yield editCell(id, "name", "newTest3");
+
+ id = getCookieId("newTest3", ".test1.example.org", "/browser");
+ yield editCell(id, "host", "test1.example.org");
+
+ id = getCookieId("newTest3", "test1.example.org", "/browser");
+ yield editCell(id, "path", "/");
+
+ id = getCookieId("newTest3", "test1.example.org", "/");
+ yield editCell(id, "expires", "Tue, 14 Feb 2040 17:41:14 GMT");
+ yield editCell(id, "value", "newValue3");
+ yield editCell(id, "isSecure", "true");
+ yield editCell(id, "isHttpOnly", "true");
yield finishTests();
});
diff --git a/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js b/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
index 1208c4376..4bbb63fbe 100644
--- a/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
+++ b/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
@@ -10,10 +10,11 @@ add_task(function* () {
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
showAllColumns(true);
- yield startCellEdit("test4", "name");
+ let id = getCookieId("test4", "test1.example.org", "/browser");
+ yield startCellEdit(id, "name");
yield typeWithTerminator("test6", "VK_TAB");
- yield typeWithTerminator("/", "VK_TAB");
yield typeWithTerminator(".example.org", "VK_TAB");
+ yield typeWithTerminator("/", "VK_TAB");
yield typeWithTerminator("Tue, 25 Dec 2040 12:00:00 GMT", "VK_TAB");
yield typeWithTerminator("test6value", "VK_TAB");
yield typeWithTerminator("false", "VK_TAB");
diff --git a/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js b/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
index 783a0c844..5da359b8d 100644
--- a/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
+++ b/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
@@ -10,7 +10,8 @@ add_task(function* () {
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
showAllColumns(true);
- yield startCellEdit("test1", "name");
+ let id = getCookieId("test1", ".test1.example.org", "/browser");
+ yield startCellEdit(id, "name");
PressKeyXTimes("VK_TAB", 18);
is(getCurrentEditorValue(), "value3",
diff --git a/devtools/client/storage/test/browser_storage_delete.js b/devtools/client/storage/test/browser_storage_delete.js
index c0e2b0ad7..a3ef7c290 100644
--- a/devtools/client/storage/test/browser_storage_delete.js
+++ b/devtools/client/storage/test/browser_storage_delete.js
@@ -13,9 +13,11 @@ const TEST_CASES = [
"ls1", "name"],
[["sessionStorage", "http://test1.example.org"],
"ss1", "name"],
- [["cookies", "test1.example.org"],
- "c1", "name"],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [
+ ["cookies", "test1.example.org"],
+ getCookieId("c1", "test1.example.org", "/browser"), "name"
+ ],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
1, "name"],
[["Cache", "http://test1.example.org", "plop"],
MAIN_DOMAIN + "404_cached_file.js", "url"],
@@ -41,7 +43,7 @@ add_task(function* () {
yield waitForContextMenu(contextMenu, row[cellToClick], () => {
info(`Opened context menu in ${treeItemName}, row '${rowName}'`);
menuDeleteItem.click();
- let truncatedRowName = String(rowName).substr(0, 16);
+ let truncatedRowName = String(rowName).replace(SEPARATOR_GUID, "-").substr(0, 16);
ok(menuDeleteItem.getAttribute("label").includes(truncatedRowName),
`Context menu item label contains '${rowName}' (maybe truncated)`);
});
diff --git a/devtools/client/storage/test/browser_storage_delete_all.js b/devtools/client/storage/test/browser_storage_delete_all.js
index c4b6048fb..60b417bdb 100644
--- a/devtools/client/storage/test/browser_storage_delete_all.js
+++ b/devtools/client/storage/test/browser_storage_delete_all.js
@@ -29,7 +29,7 @@ add_task(function* () {
["iframe-u-ss1", "iframe-u-ss2"]],
[["sessionStorage", "https://sectest1.example.org"],
["iframe-s-ss1"]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
[1, 2, 3]],
[["Cache", "http://test1.example.org", "plop"],
[MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]],
@@ -41,7 +41,7 @@ add_task(function* () {
const deleteHosts = [
[["localStorage", "https://sectest1.example.org"], "iframe-s-ls1", "name"],
[["sessionStorage", "https://sectest1.example.org"], "iframe-s-ss1", "name"],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"], 1, "name"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], 1, "name"],
[["Cache", "http://test1.example.org", "plop"],
MAIN_DOMAIN + "404_cached_file.js", "url"],
];
@@ -78,7 +78,7 @@ add_task(function* () {
["iframe-u-ss1", "iframe-u-ss2"]],
[["sessionStorage", "https://sectest1.example.org"],
[]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
[]],
[["Cache", "http://test1.example.org", "plop"],
[]],
diff --git a/devtools/client/storage/test/browser_storage_delete_tree.js b/devtools/client/storage/test/browser_storage_delete_tree.js
index 867a1c8b6..2bca4c344 100644
--- a/devtools/client/storage/test/browser_storage_delete_tree.js
+++ b/devtools/client/storage/test/browser_storage_delete_tree.js
@@ -17,10 +17,20 @@ add_task(function* () {
info("test state before delete");
yield checkState([
- [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]],
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c3", "test1.example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/")
+ ]
+ ],
[["localStorage", "http://test1.example.org"], ["ls1", "ls2"]],
[["sessionStorage", "http://test1.example.org"], ["ss1"]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"], [1, 2, 3]],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], [1, 2, 3]],
[["Cache", "http://test1.example.org", "plop"],
[MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]],
]);
@@ -30,7 +40,7 @@ add_task(function* () {
["cookies", "test1.example.org"],
["localStorage", "http://test1.example.org"],
["sessionStorage", "http://test1.example.org"],
- ["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
["Cache", "http://test1.example.org", "plop"],
];
@@ -59,7 +69,7 @@ add_task(function* () {
[["cookies", "test1.example.org"], []],
[["localStorage", "http://test1.example.org"], []],
[["sessionStorage", "http://test1.example.org"], []],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"], []],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], []],
[["Cache", "http://test1.example.org", "plop"], []],
]);
diff --git a/devtools/client/storage/test/browser_storage_dom_cache_disabled.js b/devtools/client/storage/test/browser_storage_dom_cache_disabled.js
new file mode 100644
index 000000000..db0aca392
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dom_cache_disabled.js
@@ -0,0 +1,37 @@
+/* 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 ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Test the storage inspector when dom.caches.enabled=false.
+
+add_task(function* () {
+ // Disable the DOM cache
+ Services.prefs.setBoolPref(DOM_CACHE, false);
+
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ const state = [
+ [["localStorage", "http://test1.example.org"],
+ ["ls1", "ls2"]],
+ [["localStorage", "http://sectest1.example.org"],
+ ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"],
+ ["iframe-s-ls1"]],
+ [["sessionStorage", "http://test1.example.org"],
+ ["ss1"]],
+ [["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1", "iframe-u-ss2"]],
+ [["sessionStorage", "https://sectest1.example.org"],
+ ["iframe-s-ss1"]],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
+ [1, 2, 3]],
+ ];
+
+ yield checkState(state);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js b/devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js
new file mode 100644
index 000000000..6cf52f2d3
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js
@@ -0,0 +1,188 @@
+/* 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";
+
+// Test dynamic updates in the storage inspector for cookies.
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-updates.html");
+
+ gUI.tree.expandAll();
+
+ ok(gUI.sidebar.hidden, "Sidebar is initially hidden");
+ let c1id = getCookieId("c1", "test1.example.org", "/browser");
+ yield selectTableItem(c1id);
+
+ // test that value is something initially
+ let initialValue = [[
+ {name: "c1", value: "1.2.3.4.5.6.7"},
+ {name: "c1.Path", value: "/browser"}
+ ], [
+ {name: "c1", value: "Array"},
+ {name: "c1.0", value: "1"},
+ {name: "c1.6", value: "7"}
+ ]];
+
+ // test that value is something initially
+ let finalValue = [[
+ {name: "c1", value: '{"foo": 4,"bar":6}'},
+ {name: "c1.Path", value: "/browser"}
+ ], [
+ {name: "c1", value: "Object"},
+ {name: "c1.foo", value: "4"},
+ {name: "c1.bar", value: "6"}
+ ]];
+
+ // Check that sidebar shows correct initial value
+ yield findVariableViewProperties(initialValue[0], false);
+
+ yield findVariableViewProperties(initialValue[1], true);
+ // Check if table shows correct initial value
+
+ yield checkState([
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("c2", "test1.example.org", "/browser")
+ ]
+ ],
+ ]);
+ checkCell(c1id, "value", "1.2.3.4.5.6.7");
+
+ gWindow.addCookie("c1", '{"foo": 4,"bar":6}', "/browser");
+ yield gUI.once("sidebar-updated");
+
+ yield findVariableViewProperties(finalValue[0], false);
+ yield findVariableViewProperties(finalValue[1], true);
+
+ yield checkState([
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("c2", "test1.example.org", "/browser")
+ ]
+ ],
+ ]);
+ checkCell(c1id, "value", '{"foo": 4,"bar":6}');
+
+ // Add a new entry
+ gWindow.addCookie("c3", "booyeah");
+
+ // Wait once for update and another time for value fetching
+ yield gUI.once("store-objects-updated");
+ yield gUI.once("store-objects-updated");
+
+ yield checkState([
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("c2", "test1.example.org", "/browser"),
+ getCookieId("c3", "test1.example.org",
+ "/browser/devtools/client/storage/test/")
+ ]
+ ],
+ ]);
+ let c3id = getCookieId("c3", "test1.example.org",
+ "/browser/devtools/client/storage/test/");
+ checkCell(c3id, "value", "booyeah");
+
+ // Add another
+ gWindow.addCookie("c4", "booyeah");
+
+ // Wait once for update and another time for value fetching
+ yield gUI.once("store-objects-updated");
+ yield gUI.once("store-objects-updated");
+
+ yield checkState([
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("c2", "test1.example.org", "/browser"),
+ getCookieId("c3", "test1.example.org",
+ "/browser/devtools/client/storage/test/"),
+ getCookieId("c4", "test1.example.org",
+ "/browser/devtools/client/storage/test/")
+ ]
+ ],
+ ]);
+ let c4id = getCookieId("c4", "test1.example.org",
+ "/browser/devtools/client/storage/test/");
+ checkCell(c4id, "value", "booyeah");
+
+ // Removing cookies
+ gWindow.removeCookie("c1", "/browser");
+
+ yield gUI.once("sidebar-updated");
+
+ yield checkState([
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("c2", "test1.example.org", "/browser"),
+ getCookieId("c3", "test1.example.org",
+ "/browser/devtools/client/storage/test/"),
+ getCookieId("c4", "test1.example.org",
+ "/browser/devtools/client/storage/test/")
+ ]
+ ],
+ ]);
+
+ ok(!gUI.sidebar.hidden, "Sidebar still visible for next row");
+
+ // Check if next element's value is visible in sidebar
+ yield findVariableViewProperties([{name: "c2", value: "foobar"}]);
+
+ // Keep deleting till no rows
+ gWindow.removeCookie("c3");
+
+ yield gUI.once("store-objects-updated");
+
+ yield checkState([
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("c2", "test1.example.org", "/browser"),
+ getCookieId("c4", "test1.example.org",
+ "/browser/devtools/client/storage/test/")
+ ]
+ ],
+ ]);
+
+ // Check if next element's value is visible in sidebar
+ yield findVariableViewProperties([{name: "c2", value: "foobar"}]);
+
+ gWindow.removeCookie("c2", "/browser");
+
+ yield gUI.once("sidebar-updated");
+
+ yield checkState([
+ [
+ ["cookies", "test1.example.org"],
+ [
+ getCookieId("c4", "test1.example.org",
+ "/browser/devtools/client/storage/test/")
+ ]
+ ],
+ ]);
+
+ // Check if next element's value is visible in sidebar
+ yield findVariableViewProperties([{name: "c4", value: "booyeah"}]);
+
+ gWindow.removeCookie("c4");
+
+ yield gUI.once("store-objects-updated");
+
+ yield checkState([
+ [["cookies", "test1.example.org"], [ ]],
+ ]);
+
+ ok(gUI.sidebar.hidden, "Sidebar is hidden when no rows");
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js b/devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js
new file mode 100644
index 000000000..35912ce3a
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js
@@ -0,0 +1,70 @@
+/* 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";
+
+// Test dynamic updates in the storage inspector for localStorage.
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-updates.html");
+
+ gUI.tree.expandAll();
+
+ ok(gUI.sidebar.hidden, "Sidebar is initially hidden");
+
+ yield checkState([
+ [
+ ["localStorage", "http://test1.example.org"],
+ ["ls1", "ls2", "ls3", "ls4", "ls5", "ls6", "ls7"]
+ ],
+ ]);
+
+ gWindow.localStorage.removeItem("ls4");
+
+ yield gUI.once("store-objects-updated");
+
+ yield checkState([
+ [
+ ["localStorage", "http://test1.example.org"],
+ ["ls1", "ls2", "ls3", "ls5", "ls6", "ls7"]
+ ],
+ ]);
+
+ gWindow.localStorage.setItem("ls4", "again");
+
+ yield gUI.once("store-objects-updated");
+ yield gUI.once("store-objects-updated");
+
+ yield checkState([
+ [
+ ["localStorage", "http://test1.example.org"],
+ ["ls1", "ls2", "ls3", "ls4", "ls5", "ls6", "ls7"]
+ ],
+ ]);
+ // Updating a row
+ gWindow.localStorage.setItem("ls2", "ls2-changed");
+
+ yield gUI.once("store-objects-updated");
+ yield gUI.once("store-objects-updated");
+
+ checkCell("ls2", "value", "ls2-changed");
+
+ // Clearing items. Bug 1233497 makes it so that we can no longer yield
+ // CPOWs from Tasks. We work around this by calling clear via a ContentTask
+ // instead.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ return Task.spawn(content.wrappedJSObject.clear);
+ });
+
+ yield gUI.once("store-objects-cleared");
+
+ yield checkState([
+ [
+ ["localStorage", "http://test1.example.org"],
+ [ ]
+ ],
+ ]);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js b/devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js
new file mode 100644
index 000000000..8c2f2537e
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js
@@ -0,0 +1,83 @@
+/* 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";
+
+// Test dynamic updates in the storage inspector for sessionStorage.
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-updates.html");
+
+ gUI.tree.expandAll();
+
+ ok(gUI.sidebar.hidden, "Sidebar is initially hidden");
+ yield checkState([
+ [
+ ["sessionStorage", "http://test1.example.org"],
+ ["ss1", "ss2", "ss3"]
+ ],
+ ]);
+
+ gWindow.sessionStorage.setItem("ss4", "new-item");
+
+ yield gUI.once("store-objects-updated");
+ yield gUI.once("store-objects-updated");
+
+ yield checkState([
+ [
+ ["sessionStorage", "http://test1.example.org"],
+ ["ss1", "ss2", "ss3", "ss4"]
+ ],
+ ]);
+
+ // deleting item
+
+ gWindow.sessionStorage.removeItem("ss3");
+
+ yield gUI.once("store-objects-updated");
+
+ gWindow.sessionStorage.removeItem("ss1");
+
+ yield gUI.once("store-objects-updated");
+
+ yield checkState([
+ [
+ ["sessionStorage", "http://test1.example.org"],
+ ["ss2", "ss4"]
+ ],
+ ]);
+
+ yield selectTableItem("ss2");
+
+ ok(!gUI.sidebar.hidden, "sidebar is visible");
+
+ // Checking for correct value in sidebar before update
+ yield findVariableViewProperties([{name: "ss2", value: "foobar"}]);
+
+ gWindow.sessionStorage.setItem("ss2", "changed=ss2");
+
+ yield gUI.once("sidebar-updated");
+
+ checkCell("ss2", "value", "changed=ss2");
+
+ yield findVariableViewProperties([{name: "ss2", value: "changed=ss2"}]);
+
+ // Clearing items. Bug 1233497 makes it so that we can no longer yield
+ // CPOWs from Tasks. We work around this by calling clear via a ContentTask
+ // instead.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ return Task.spawn(content.wrappedJSObject.clear);
+ });
+
+ yield gUI.once("store-objects-cleared");
+
+ yield checkState([
+ [
+ ["sessionStorage", "http://test1.example.org"],
+ [ ]
+ ],
+ ]);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_empty_objectstores.js b/devtools/client/storage/test/browser_storage_empty_objectstores.js
index 1749c91b8..e6f259742 100644
--- a/devtools/client/storage/test/browser_storage_empty_objectstores.js
+++ b/devtools/client/storage/test/browser_storage_empty_objectstores.js
@@ -21,14 +21,14 @@
// storage-secured-iframe.html and storage-unsecured-iframe.html
const storeItems = [
[["indexedDB", "http://test1.example.org"],
- ["idb1", "idb2"]],
- [["indexedDB", "http://test1.example.org", "idb1"],
+ ["idb1 (default)", "idb2 (default)"]],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)"],
["obj1", "obj2"]],
- [["indexedDB", "http://test1.example.org", "idb2"],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)"],
[]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
[1, 2, 3]],
- [["indexedDB", "http://test1.example.org", "idb1", "obj2"],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"],
[1]]
];
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_delete.js b/devtools/client/storage/test/browser_storage_indexeddb_delete.js
index 18a0daf69..5c499c9e9 100644
--- a/devtools/client/storage/test/browser_storage_indexeddb_delete.js
+++ b/devtools/client/storage/test/browser_storage_indexeddb_delete.js
@@ -16,11 +16,11 @@ add_task(function* () {
info("test state before delete");
yield checkState([
- [["indexedDB", "http://test1.example.org"], ["idb1", "idb2"]],
+ [["indexedDB", "http://test1.example.org"], ["idb1 (default)", "idb2 (default)"]],
]);
info("do the delete");
- const deletedDb = ["indexedDB", "http://test1.example.org", "idb1"];
+ const deletedDb = ["indexedDB", "http://test1.example.org", "idb1 (default)"];
yield selectTreeItem(deletedDb);
@@ -40,7 +40,7 @@ add_task(function* () {
info("test state after delete");
yield checkState([
- [["indexedDB", "http://test1.example.org"], ["idb2"]],
+ [["indexedDB", "http://test1.example.org"], ["idb2 (default)"]],
]);
yield finishTests();
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js b/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js
index 6e89c4f28..2d77896f3 100644
--- a/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js
+++ b/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js
@@ -13,19 +13,19 @@ add_task(function* () {
info("test state before delete");
yield checkState([
- [["indexedDB", "http://test1.example.org"], ["idb"]]
+ [["indexedDB", "http://test1.example.org"], ["idb (default)"]]
]);
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");
+ let front = gUI.getCurrentFront();
+ let result = yield front.removeDatabase("http://test1.example.org", "idb (default)");
ok(result.blocked, "removeDatabase attempt is blocked");
info("test state after blocked delete");
yield checkState([
- [["indexedDB", "http://test1.example.org"], ["idb"]]
+ [["indexedDB", "http://test1.example.org"], ["idb (default)"]]
]);
let eventWait = gUI.once("store-objects-updated");
@@ -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");
+ result = yield front.removeDatabase("http://test2.example.org", "idb (default)");
} catch (ex) {
errorThrown = true;
}
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js b/devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js
new file mode 100644
index 000000000..8316d22c5
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js
@@ -0,0 +1,31 @@
+/* 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 to verify that indexedDBs with duplicate names (different types / paths)
+// work as expected.
+
+"use strict";
+
+add_task(function* () {
+ const TESTPAGE = MAIN_DOMAIN + "storage-indexeddb-duplicate-names.html";
+
+ setPermission(TESTPAGE, "indexedDB");
+
+ yield openTabAndSetupStorage(TESTPAGE);
+
+ yield checkState([
+ [
+ ["indexedDB", "http://test1.example.org"], [
+ "idb1 (default)",
+ "idb1 (temporary)",
+ "idb1 (persistent)",
+ "idb2 (default)",
+ "idb2 (temporary)",
+ "idb2 (persistent)"
+ ]
+ ]
+ ]);
+
+ yield finishTests();
+});
diff --git a/devtools/client/storage/test/browser_storage_localstorage_add.js b/devtools/client/storage/test/browser_storage_localstorage_add.js
new file mode 100644
index 000000000..de40957b8
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_localstorage_add.js
@@ -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();
+});
diff --git a/devtools/client/storage/test/browser_storage_overflow.js b/devtools/client/storage/test/browser_storage_overflow.js
index 88181ca05..21b931c8e 100644
--- a/devtools/client/storage/test/browser_storage_overflow.js
+++ b/devtools/client/storage/test/browser_storage_overflow.js
@@ -2,40 +2,58 @@
// inspector table.
"use strict";
+const ITEMS_PER_PAGE = 50;
+
add_task(function* () {
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-overflow.html");
- let $ = id => gPanelWindow.document.querySelector(id);
- let $$ = sel => gPanelWindow.document.querySelectorAll(sel);
-
gUI.tree.expandAll();
yield selectTreeItem(["localStorage", "http://test1.example.org"]);
+ checkCellLength(ITEMS_PER_PAGE);
+
+ yield scroll();
+ checkCellLength(ITEMS_PER_PAGE * 2);
- let table = $("#storage-table .table-widget-body");
- let cellHeight = $(".table-widget-cell").getBoundingClientRect().height;
+ yield scroll();
+ checkCellLength(ITEMS_PER_PAGE * 3);
- is($$("#value .table-widget-cell").length, 50,
- "Table should initially display 50 items");
+ // Check that the columns are sorted in a human readable way (ascending).
+ checkCellValues("ASC");
- let onStoresUpdate = gUI.once("store-objects-updated");
- table.scrollTop += cellHeight * 50;
- yield onStoresUpdate;
+ // Sort descending.
+ clickColumnHeader("name");
- is($$("#value .table-widget-cell").length, 100,
- "Table should display 100 items after scrolling");
+ // Check that the columns are sorted in a human readable way (descending).
+ checkCellValues("DEC");
- onStoresUpdate = gUI.once("store-objects-updated");
- table.scrollTop += cellHeight * 50;
- yield onStoresUpdate;
+ yield finishTests();
+});
- is($$("#value .table-widget-cell").length, 150,
- "Table should display 150 items after scrolling");
+function checkCellLength(len) {
+ let cells = gPanelWindow.document
+ .querySelectorAll("#name .table-widget-cell");
+ let msg = `Table should initially display ${len} items`;
- onStoresUpdate = gUI.once("store-objects-updated");
+ is(cells.length, len, msg);
+}
+
+function checkCellValues(order) {
+ let cells = [...gPanelWindow.document
+ .querySelectorAll("#name .table-widget-cell")];
+ cells.forEach(function (cell, index, arr) {
+ let i = order === "ASC" ? index + 1 : arr.length - index;
+ is(cell.value, `item-${i}`, `Cell value is correct (${order}).`);
+ });
+}
+
+function* scroll() {
+ let $ = id => gPanelWindow.document.querySelector(id);
+
+ let table = $("#storage-table .table-widget-body");
+ let cell = $("#name .table-widget-cell");
+ let cellHeight = cell.getBoundingClientRect().height;
+
+ let onStoresUpdate = gUI.once("store-objects-updated");
table.scrollTop += cellHeight * 50;
yield onStoresUpdate;
-
- is($$("#value .table-widget-cell").length, 160,
- "Table should display all 160 items after scrolling");
- yield finishTests();
-});
+}
diff --git a/devtools/client/storage/test/browser_storage_sessionstorage_add.js b/devtools/client/storage/test/browser_storage_sessionstorage_add.js
new file mode 100644
index 000000000..8f220bc81
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sessionstorage_add.js
@@ -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();
+});
diff --git a/devtools/client/storage/test/browser_storage_sidebar.js b/devtools/client/storage/test/browser_storage_sidebar.js
index 9b60026a0..6712ac013 100644
--- a/devtools/client/storage/test/browser_storage_sidebar.js
+++ b/devtools/client/storage/test/browser_storage_sidebar.js
@@ -20,22 +20,22 @@ const testCases = [
sidebarHidden: true
},
{
- location: "cs2",
+ location: getCookieId("cs2", ".example.org", "/"),
sidebarHidden: false
},
{
sendEscape: true
},
{
- location: "cs2",
+ location: getCookieId("cs2", ".example.org", "/"),
sidebarHidden: false
},
{
- location: "uc1",
+ location: getCookieId("uc1", ".example.org", "/"),
sidebarHidden: false
},
{
- location: "uc1",
+ location: getCookieId("uc1", ".example.org", "/"),
sidebarHidden: false
},
@@ -72,17 +72,17 @@ const testCases = [
sidebarHidden: true
},
{
- location: "idb2",
+ location: "idb2 (default)",
sidebarHidden: false
},
{
- location: ["indexedDB", "http://test1.example.org", "idb2", "obj3"],
+ location: ["indexedDB", "http://test1.example.org", "idb2 (default)", "obj3"],
sidebarHidden: true
},
{
- location: ["indexedDB", "https://sectest1.example.org", "idb-s2"],
+ location: ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"],
sidebarHidden: true
},
{
diff --git a/devtools/client/storage/test/browser_storage_sidebar_update.js b/devtools/client/storage/test/browser_storage_sidebar_update.js
index 419d63020..92547815a 100644
--- a/devtools/client/storage/test/browser_storage_sidebar_update.js
+++ b/devtools/client/storage/test/browser_storage_sidebar_update.js
@@ -26,7 +26,7 @@ add_task(function* () {
for (let i = 0; i < UPDATE_COUNT; i++) {
info(`Performing update #${i}`);
updates.push(gUI.once("sidebar-updated"));
- gUI.displayObjectSidebar();
+ gUI.updateObjectSidebar();
}
yield promise.all(updates);
diff --git a/devtools/client/storage/test/browser_storage_values.js b/devtools/client/storage/test/browser_storage_values.js
index 920ce350e..1d3e9ff76 100644
--- a/devtools/client/storage/test/browser_storage_values.js
+++ b/devtools/client/storage/test/browser_storage_values.js
@@ -17,7 +17,7 @@
const LONG_WORD = "a".repeat(1000);
const testCases = [
- ["cs2", [
+ [getCookieId("cs2", ".example.org", "/"), [
{name: "cs2", value: "sessionCookie"},
{name: "cs2.Path", value: "/"},
{name: "cs2.HostOnly", value: "false"},
@@ -26,7 +26,7 @@ const testCases = [
{name: "cs2.Expires", value: "Session"},
{name: "cs2.Secure", value: "false"},
]],
- ["c1", [
+ [getCookieId("c1", "test1.example.org", "/browser"), [
{name: "c1", value: JSON.stringify(["foo", "Bar", {foo: "Bar"}])},
{name: "c1.Path", value: "/browser"},
{name: "c1.HostOnly", value: "true"},
@@ -42,9 +42,13 @@ const testCases = [
{name: "c1.2", value: "Object"},
{name: "c1.2.foo", value: "Bar"},
], true],
- ["c_encoded", [
- {name: "c_encoded", value: encodeURIComponent(JSON.stringify({foo: {foo1: "bar"}}))}
- ]],
+ [
+ getCookieId("c_encoded", "test1.example.org",
+ "/browser/devtools/client/storage/test/"),
+ [
+ {name: "c_encoded", value: encodeURIComponent(JSON.stringify({foo: {foo1: "bar"}}))}
+ ]
+ ],
[null, [
{name: "c_encoded", value: "Object"},
{name: "c_encoded.foo", value: "Object"},
@@ -120,7 +124,7 @@ const testCases = [
{name: "ss5.3", value: `${LONG_WORD}&${LONG_WORD}`},
{name: "ss5.4", value: `${LONG_WORD}&${LONG_WORD}`},
], true],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"]],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"]],
[1, [
{name: 1, value: JSON.stringify({id: 1, name: "foo", email: "foo@bar.com"})}
]],
@@ -129,7 +133,7 @@ const testCases = [
{name: "1.name", value: "foo"},
{name: "1.email", value: "foo@bar.com"},
], true],
- [["indexedDB", "http://test1.example.org", "idb1", "obj2"]],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"]],
[1, [
{name: 1, value: JSON.stringify({
id2: 1, name: "foo", email: "foo@bar.com", extra: "baz"
diff --git a/devtools/client/storage/test/head.js b/devtools/client/storage/test/head.js
index 9662393cf..181132a45 100644
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -15,6 +15,7 @@ Services.scriptloader.loadSubScript(
const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
const SPLIT_CONSOLE_PREF = "devtools.toolbox.splitconsoleEnabled";
const STORAGE_PREF = "devtools.storage.enabled";
+const DOM_CACHE = "dom.caches.enabled";
const DUMPEMIT_PREF = "devtools.dump.emit";
const DEBUGGERLOG_PREF = "devtools.debugger.log";
// Allows Cache API to be working on usage `http` test page
@@ -24,6 +25,11 @@ const MAIN_DOMAIN = "http://test1.example.org/" + PATH;
const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH;
+// GUID to be used as a separator in compound keys. This must match the same
+// constant in devtools/server/actors/storage.js,
+// devtools/client/storage/ui.js and devtools/server/tests/browser/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
var gToolbox, gPanelWindow, gWindow, gUI;
// Services.prefs.setBoolPref(DUMPEMIT_PREF, true);
@@ -33,11 +39,12 @@ Services.prefs.setBoolPref(STORAGE_PREF, true);
Services.prefs.setBoolPref(CACHES_ON_HTTP_PREF, true);
registerCleanupFunction(() => {
gToolbox = gPanelWindow = gWindow = gUI = null;
- Services.prefs.clearUserPref(STORAGE_PREF);
- Services.prefs.clearUserPref(SPLIT_CONSOLE_PREF);
- Services.prefs.clearUserPref(DUMPEMIT_PREF);
- Services.prefs.clearUserPref(DEBUGGERLOG_PREF);
Services.prefs.clearUserPref(CACHES_ON_HTTP_PREF);
+ Services.prefs.clearUserPref(DEBUGGERLOG_PREF);
+ Services.prefs.clearUserPref(DOM_CACHE);
+ Services.prefs.clearUserPref(DUMPEMIT_PREF);
+ Services.prefs.clearUserPref(SPLIT_CONSOLE_PREF);
+ Services.prefs.clearUserPref(STORAGE_PREF);
});
/**
@@ -505,10 +512,16 @@ function* selectTreeItem(ids) {
* The id of the row in the table widget
*/
function* selectTableItem(id) {
- let selector = ".table-widget-cell[data-id='" + id + "']";
+ let table = gUI.table;
+ let selector = ".table-widget-column#" + table.uniqueId +
+ " .table-widget-cell[value='" + id + "']";
let target = gPanelWindow.document.querySelector(selector);
ok(target, "table item found with ids " + id);
+ if (!target) {
+ showAvailableIds();
+ }
+
yield click(target);
yield gUI.once("sidebar-updated");
}
@@ -586,22 +599,39 @@ function getRowCells(id, includeHidden = false) {
if (!item) {
ok(false, "Row id '" + id + "' exists");
+
+ showAvailableIds();
}
- let index = table.columns.get(table.uniqueId).visibleCellNodes.indexOf(item);
+ let index = table.columns.get(table.uniqueId).cellNodes.indexOf(item);
let cells = {};
for (let [name, column] of [...table.columns]) {
if (!includeHidden && column.column.parentNode.hidden) {
continue;
}
- cells[name] = column.visibleCellNodes[index];
+ cells[name] = column.cellNodes[index];
}
return cells;
}
/**
+ * Show available ids.
+ */
+function showAvailableIds() {
+ let doc = gPanelWindow.document;
+ let table = gUI.table;
+
+ info("Available ids:");
+ let cells = doc.querySelectorAll(".table-widget-column#" + table.uniqueId +
+ " .table-widget-cell");
+ for (let cell of cells) {
+ info(" - " + cell.getAttribute("value"));
+ }
+}
+
+/**
* Get a cell value.
*
* @param {String} id
@@ -704,6 +734,20 @@ function showColumn(id, state) {
}
/**
+ * Toggle sort direction on a column by clicking on the column header.
+ *
+ * @param {String} id
+ * The uniqueId of the given column.
+ */
+function clickColumnHeader(id) {
+ let columns = gUI.table.columns;
+ let column = columns.get(id);
+ let header = column.header;
+
+ header.click();
+}
+
+/**
* Show or hide all columns.
*
* @param {Boolean} state
@@ -798,9 +842,18 @@ function* checkState(state) {
is(items.size, names.length,
`There is correct number of rows in ${storeName}`);
+
+ if (names.length === 0) {
+ showAvailableIds();
+ }
+
for (let name of names) {
ok(items.has(name),
`There is item with name '${name}' in ${storeName}`);
+
+ if (!items.has(name)) {
+ showAvailableIds();
+ }
}
}
}
@@ -838,3 +891,59 @@ var focusSearchBoxUsingShortcut = Task.async(function* (panelWin, callback) {
callback();
}
});
+
+function getCookieId(name, domain, path) {
+ return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`;
+}
+
+function setPermission(url, permission) {
+ const nsIPermissionManager = Components.interfaces.nsIPermissionManager;
+
+ let uri = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService)
+ .newURI(url, null, null);
+ let ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Ci.nsIScriptSecurityManager);
+ let principal = ssm.createCodebasePrincipal(uri, {});
+
+ Components.classes["@mozilla.org/permissionmanager;1"]
+ .getService(nsIPermissionManager)
+ .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.`);
+}
diff --git a/devtools/client/storage/test/storage-indexeddb-duplicate-names.html b/devtools/client/storage/test/storage-indexeddb-duplicate-names.html
new file mode 100644
index 000000000..d8c76dc2a
--- /dev/null
+++ b/devtools/client/storage/test/storage-indexeddb-duplicate-names.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta charset="utf-8">
+ <title>Storage inspector IndexedDBs with duplicate names</title>
+
+ <script type="application/javascript;version=1.7">
+ "use strict";
+
+ function createIndexedDBs() {
+ createIndexedDB("idb1", "temporary");
+ createIndexedDB("idb1", "default");
+ createIndexedDB("idb1", "persistent");
+ createIndexedDB("idb2", "temporary");
+ createIndexedDB("idb2", "default");
+ createIndexedDB("idb2", "persistent");
+ }
+
+ function createIndexedDB(name, storage) {
+ let open = indexedDB.open(name, {storage: storage});
+
+ open.onsuccess = function () {
+ let db = open.result;
+ db.close();
+ };
+ }
+
+ function deleteDB(dbName, storage) {
+ return new Promise(resolve => {
+ dump(`removing database ${dbName} (${storage}) from ${document.location}\n`);
+ indexedDB.deleteDatabase(dbName, { storage: storage }).onsuccess = resolve;
+ });
+ }
+
+ window.clear = function* () {
+ yield deleteDB("idb1", "temporary");
+ yield deleteDB("idb1", "default");
+ yield deleteDB("idb1", "persistent");
+ yield deleteDB("idb2", "temporary");
+ yield deleteDB("idb2", "default");
+ yield deleteDB("idb2", "persistent");
+
+ dump(`removed indexedDB data from ${document.location}\n`);
+ };
+ </script>
+</head>
+<body onload="createIndexedDBs()">
+ <h1>storage-indexeddb-duplicate-names.html</h1>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-listings.html b/devtools/client/storage/test/storage-listings.html
index de3054d3a..313b36b71 100644
--- a/devtools/client/storage/test/storage-listings.html
+++ b/devtools/client/storage/test/storage-listings.html
@@ -1,4 +1,4 @@
-<!DOCTYPE HTML>
+<!DOCTYPE HTML>
<html>
<!--
Bug 970517 - Storage inspector front end - tests
@@ -20,7 +20,10 @@ document.cookie = "c1=foobar; expires=" +
new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
document.cookie = "c3=foobar-2; expires=" +
- new Date(cookieExpiresTime2).toGMTString() + "; path=/";
+ new Date(cookieExpiresTime1).toGMTString() + "; path=/";
+document.cookie = "c4=foobar-3; expires=" +
+ new Date(cookieExpiresTime2).toGMTString() + "; path=/; domain=" +
+ partialHostname;
// ... and some local storage items ..
localStorage.setItem("ls1", "foobar");
localStorage.setItem("ls2", "foobar-2");
@@ -110,14 +113,19 @@ let cacheGenerator = function*() {
window.setup = function*() {
yield idbGenerator();
- yield cacheGenerator();
+
+ if (window.caches) {
+ yield cacheGenerator();
+ }
};
window.clear = function*() {
yield deleteDB("idb1");
yield deleteDB("idb2");
- yield caches.delete("plop");
+ if (window.caches) {
+ yield caches.delete("plop");
+ }
dump("removed indexedDB and cache data from " + document.location + "\n");
};
diff --git a/devtools/client/storage/test/storage-overflow.html b/devtools/client/storage/test/storage-overflow.html
index ee8db36e6..6309046b3 100644
--- a/devtools/client/storage/test/storage-overflow.html
+++ b/devtools/client/storage/test/storage-overflow.html
@@ -11,7 +11,7 @@ Bug 1171903 - Storage Inspector endless scrolling
<script type="text/javascript;version=1.8">
"use strict";
-for (let i = 0; i < 160; i++) {
+for (let i = 1; i < 151; i++) {
localStorage.setItem(`item-${i}`, `value-${i}`);
}
</script>
diff --git a/devtools/client/storage/test/storage-secured-iframe.html b/devtools/client/storage/test/storage-secured-iframe.html
index 8424fd4cd..9e1ef60a0 100644
--- a/devtools/client/storage/test/storage-secured-iframe.html
+++ b/devtools/client/storage/test/storage-secured-iframe.html
@@ -1,4 +1,4 @@
-<!DOCTYPE HTML>
+<!DOCTYPE HTML>
<html>
<!--
Iframe for testing multiple host detetion in storage actor
@@ -9,7 +9,10 @@ Iframe for testing multiple host detetion in storage actor
<body>
<script type="application/javascript;version=1.7">
"use strict";
+let cookieExpiresTime = 2000000000000;
document.cookie = "sc1=foobar;";
+document.cookie = "sc2=foobar-2; expires=" +
+ new Date(cookieExpiresTime).toGMTString() + ";";
localStorage.setItem("iframe-s-ls1", "foobar");
sessionStorage.setItem("iframe-s-ss1", "foobar-2");
dump("added cookies and storage from secured iframe\n");
diff --git a/devtools/client/storage/test/storage-unsecured-iframe.html b/devtools/client/storage/test/storage-unsecured-iframe.html
index a69ffdfd1..cd08a6164 100644
--- a/devtools/client/storage/test/storage-unsecured-iframe.html
+++ b/devtools/client/storage/test/storage-unsecured-iframe.html
@@ -9,7 +9,10 @@ Iframe for testing multiple host detetion in storage actor
<body>
<script>
"use strict";
+let cookieExpiresTime = 2000000000000;
document.cookie = "uc1=foobar; domain=.example.org; path=/";
+document.cookie = "uc2=foobar-2; expires=" +
+ new Date(cookieExpiresTime).toGMTString() + "; path=/; domain=.example.org";
localStorage.setItem("iframe-u-ls1", "foobar");
sessionStorage.setItem("iframe-u-ss1", "foobar1");
sessionStorage.setItem("iframe-u-ss2", "foobar2");
diff --git a/devtools/client/storage/test/storage-updates.html b/devtools/client/storage/test/storage-updates.html
index a009814b2..341992f61 100644
--- a/devtools/client/storage/test/storage-updates.html
+++ b/devtools/client/storage/test/storage-updates.html
@@ -38,8 +38,10 @@ window.removeCookie = function(name, path) {
* can be tested.
*/
window.clear = function*() {
- sessionStorage.clear();
+ localStorage.clear();
+ dump("removed localStorage from " + document.location + "\n");
+ sessionStorage.clear();
dump("removed sessionStorage from " + document.location + "\n");
};
diff --git a/devtools/client/storage/ui.js b/devtools/client/storage/ui.js
index 6af493e44..ef07a8e88 100644
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -12,6 +12,12 @@ const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
const JSOL = require("devtools/client/shared/vendor/jsol");
const {KeyCodes} = require("devtools/client/shared/keycodes");
+// GUID to be used as a separator in compound keys. This must match the same
+// constant in devtools/server/actors/storage.js,
+// devtools/client/storage/test/head.js and
+// devtools/server/tests/browser/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
loader.lazyRequireGetter(this, "TreeWidget",
"devtools/client/shared/widgets/TreeWidget", true);
loader.lazyRequireGetter(this, "TableWidget",
@@ -36,13 +42,6 @@ const GENERIC_VARIABLES_VIEW_SETTINGS = {
preventDescriptorModifiers: true
};
-// Columns which are hidden by default in the storage table
-const HIDDEN_COLUMNS = [
- "creationTime",
- "isDomain",
- "isSecure"
-];
-
const REASON = {
NEW_ROW: "new-row",
NEXT_50_ITEMS: "next-50-items",
@@ -114,8 +113,8 @@ function StorageUI(front, target, panelWin, toolbox) {
cellContextMenuId: "storage-table-popup"
});
- this.displayObjectSidebar = this.displayObjectSidebar.bind(this);
- this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar);
+ this.updateObjectSidebar = this.updateObjectSidebar.bind(this);
+ this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.updateObjectSidebar);
this.handleScrollEnd = this.handleScrollEnd.bind(this);
this.table.on(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd);
@@ -161,11 +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);
@@ -179,10 +187,20 @@ function StorageUI(front, target, panelWin, toolbox) {
"storage-table-popup-delete-all");
this._tablePopupDeleteAll.addEventListener("command", this.onRemoveAll);
+ this._tablePopupDeleteAllSessionCookies = this._panelDoc.getElementById(
+ "storage-table-popup-delete-all-session-cookies");
+ this._tablePopupDeleteAllSessionCookies.addEventListener("command",
+ this.onRemoveAllSessionCookies);
+
this._treePopupDeleteAll = this._panelDoc.getElementById(
"storage-tree-popup-delete-all");
this._treePopupDeleteAll.addEventListener("command", this.onRemoveAll);
+ this._treePopupDeleteAllSessionCookies = this._panelDoc.getElementById(
+ "storage-tree-popup-delete-all-session-cookies");
+ this._treePopupDeleteAllSessionCookies.addEventListener("command",
+ this.onRemoveAllSessionCookies);
+
this._treePopupDelete = this._panelDoc.getElementById("storage-tree-popup-delete");
this._treePopupDelete.addEventListener("command", this.onRemoveTreeItem);
}
@@ -199,7 +217,7 @@ StorageUI.prototype = {
},
destroy: function () {
- this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar);
+ this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.updateObjectSidebar);
this.table.off(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd);
this.table.off(TableWidget.EVENTS.CELL_EDIT, this.editItem);
this.table.destroy();
@@ -211,13 +229,19 @@ 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);
this._treePopupDelete.removeEventListener("command", this.onRemoveTreeItem);
this._tablePopup.removeEventListener("popupshowing", this.onTablePopupShowing);
this._tablePopupDelete.removeEventListener("command", this.onRemoveItem);
this._tablePopupDeleteAllFrom.removeEventListener("command", this.onRemoveAllFrom);
this._tablePopupDeleteAll.removeEventListener("command", this.onRemoveAll);
+ this._tablePopupDeleteAllSessionCookies.removeEventListener("command",
+ this.onRemoveAllSessionCookies);
},
/**
@@ -229,7 +253,7 @@ StorageUI.prototype = {
this.table.clearSelection();
},
- getCurrentActor: function () {
+ getCurrentFront: function () {
let type = this.table.datatype;
return this.storageTypes[type];
@@ -250,9 +274,9 @@ StorageUI.prototype = {
},
editItem: function (eventType, data) {
- let actor = this.getCurrentActor();
+ let front = this.getCurrentFront();
- actor.editItem(data);
+ front.editItem(data);
},
/**
@@ -261,17 +285,16 @@ StorageUI.prototype = {
* being removed was selected.
*/
removeItemFromTable: function (name) {
- if (this.table.isSelected(name)) {
+ if (this.table.isSelected(name) && this.table.items.size > 1) {
if (this.table.selectedIndex == 0) {
this.table.selectNextRow();
} else {
this.table.selectPreviousRow();
}
- this.table.remove(name);
- this.displayObjectSidebar();
- } else {
- this.table.remove(name);
}
+
+ this.table.remove(name);
+ this.updateObjectSidebar();
},
/**
@@ -454,22 +477,24 @@ StorageUI.prototype = {
* @param {object} See onUpdate docs
*/
handleChangedItems: function (changed) {
- let [type, host, db, objectStore] = this.tree.selectedItem;
- if (!changed[type] || !changed[type][host] ||
- changed[type][host].length == 0) {
- return;
- }
- try {
- let toUpdate = [];
- for (let name of changed[type][host]) {
- let names = JSON.parse(name);
- if (names[0] == db && names[1] == objectStore && names[2]) {
- toUpdate.push(name);
+ if (this.tree.selectedItem) {
+ let [type, host, db, objectStore] = this.tree.selectedItem;
+ if (!changed[type] || !changed[type][host] ||
+ changed[type][host].length == 0) {
+ return;
+ }
+ try {
+ let toUpdate = [];
+ for (let name of changed[type][host]) {
+ let names = JSON.parse(name);
+ if (names[0] == db && names[1] == objectStore && names[2]) {
+ toUpdate.push(name);
+ }
}
+ this.fetchStorageObjects(type, host, toUpdate, REASON.UPDATE);
+ } catch (ex) {
+ this.fetchStorageObjects(type, host, changed[type][host], REASON.UPDATE);
}
- this.fetchStorageObjects(type, host, toUpdate, REASON.UPDATE);
- } catch (ex) {
- this.fetchStorageObjects(type, host, changed[type][host], REASON.UPDATE);
}
},
@@ -504,7 +529,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";
@@ -513,6 +538,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);
}
@@ -520,6 +554,7 @@ StorageUI.prototype = {
if (data.length) {
this.populateTable(data, reason);
}
+ yield this.updateToolbar();
this.emit("store-objects-updated");
} catch (ex) {
console.error(ex);
@@ -527,6 +562,27 @@ StorageUI.prototype = {
}),
/**
+ * 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.
*
@@ -574,23 +630,28 @@ StorageUI.prototype = {
},
/**
- * Populates the selected entry from teh table in the sidebar for a more
+ * Populates the selected entry from the table in the sidebar for a more
* detailed view.
*/
- displayObjectSidebar: Task.async(function* () {
+ updateObjectSidebar: Task.async(function* () {
let item = this.table.selectedRow;
- if (!item) {
- // Make sure that sidebar is hidden and return
- this.sidebar.hidden = true;
- return;
- }
+ let value;
// Get the string value (async action) and the update the UI synchronously.
- let value;
- if (item.name && item.valueActor) {
+ if (item && item.name && item.valueActor) {
value = yield item.valueActor.string();
}
+ // Bail if the selectedRow is no longer selected, the item doesn't exist or the state
+ // changed in another way during the above yield.
+ if (this.table.items.size === 0 ||
+ !item ||
+ !this.table.selectedRow ||
+ item.uniqueKey !== this.table.selectedRow.uniqueKey) {
+ this.hideSidebar();
+ return;
+ }
+
// Start updating the UI. Everything is sync beyond this point.
this.sidebar.hidden = false;
this.view.empty();
@@ -616,6 +677,11 @@ StorageUI.prototype = {
let otherProps = itemProps.filter(
e => !["name", "value", "valueActor"].includes(e));
for (let prop of otherProps) {
+ let column = this.table.columns.get(prop);
+ if (column && column.private) {
+ continue;
+ }
+
let cookieProp = COOKIE_KEY_MAP[prop] || prop;
// The pseduo property of HostOnly refers to converse of isDomain property
rawObject[cookieProp] = (prop === "isDomain") ? !item[prop] : item[prop];
@@ -627,6 +693,11 @@ StorageUI.prototype = {
} else {
// Case when displaying IndexedDB db/object store properties.
for (let key in item) {
+ let column = this.table.columns.get(key);
+ if (column && column.private) {
+ continue;
+ }
+
mainScope.addItem(key, {}, true).setGrip(item[key]);
this.parseItemValue(key, item[key]);
}
@@ -751,11 +822,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;
@@ -786,7 +865,9 @@ StorageUI.prototype = {
let uniqueKey = null;
let columns = {};
let editableFields = [];
- let fields = yield this.getCurrentActor().getFields(subtype);
+ let hiddenFields = [];
+ let privateFields = [];
+ let fields = yield this.getCurrentFront().getFields(subtype);
fields.forEach(f => {
if (!uniqueKey) {
@@ -797,10 +878,21 @@ StorageUI.prototype = {
editableFields.push(f.name);
}
+ if (f.hidden) {
+ hiddenFields.push(f.name);
+ }
+
+ if (f.private) {
+ privateFields.push(f.name);
+ }
+
columns[f.name] = f.name;
let columnName;
try {
- columnName = L10N.getStr("table.headers." + type + "." + f.name);
+ // Path key names for l10n in the case of a string change.
+ let name = f.name === "keyPath" ? "keyPath2" : f.name;
+
+ columnName = L10N.getStr("table.headers." + type + "." + name);
} catch (e) {
columnName = COOKIE_KEY_MAP[f.name];
}
@@ -812,7 +904,7 @@ StorageUI.prototype = {
}
});
- this.table.setColumns(columns, null, HIDDEN_COLUMNS);
+ this.table.setColumns(columns, null, hiddenFields, privateFields);
this.hideSidebar();
yield this.makeFieldsEditable(editableFields);
@@ -857,7 +949,7 @@ StorageUI.prototype = {
case REASON.UPDATE:
this.table.update(item);
if (item == this.table.selectedRow && !this.sidebar.hidden) {
- this.displayObjectSidebar();
+ this.updateObjectSidebar();
}
break;
}
@@ -910,27 +1002,53 @@ 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 = addEllipsis(data[this.table.uniqueId]);
- this._tablePopupDelete.setAttribute("label",
- L10N.getFormatStr("storage.popupMenu.deleteLabel", name));
+ if (this.actorSupportsRemoveItem) {
+ let name = data[this.table.uniqueId];
+ let separatorRegex = new RegExp(SEPARATOR_GUID, "g");
+ let label = addEllipsis((name + "").replace(separatorRegex, "-"));
+
+ 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 (this.actorSupportsRemoveAllSessionCookies) {
+ if (type === "cookies" && selectedItem.length === 2) {
+ showDeleteAllSessionCookies = true;
+ }
+ }
+
+ this._tablePopupDeleteAllSessionCookies.hidden = !showDeleteAllSessionCookies;
if (type === "cookies") {
let host = addEllipsis(data.host);
@@ -949,13 +1067,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;
@@ -972,6 +1089,17 @@ StorageUI.prototype = {
this._treePopupDeleteAll.hidden = !showDeleteAll;
+ // The delete all session cookies action is displayed for cookie object stores
+ // (level 2 of tree)
+ let showDeleteAllSessionCookies = false;
+ if (this.actorSupportsRemoveAllSessionCookies) {
+ if (type === "cookies" && selectedItem.length === 2) {
+ showDeleteAllSessionCookies = true;
+ }
+ }
+
+ this._treePopupDeleteAllSessionCookies.hidden = !showDeleteAllSessionCookies;
+
// The delete action is displayed for:
// - IndexedDB databases (level 3 of the tree)
// - Cache objects (level 3 of the tree)
@@ -993,31 +1121,54 @@ 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 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 () {
+ let [, host, ...path] = this.tree.selectedItem;
+ let front = this.getCurrentFront();
+ let name = path.length > 0 ? JSON.stringify(path) : undefined;
+ front.removeAllSessionCookies(host, name);
},
/**
@@ -1026,11 +1177,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 () {
@@ -1044,9 +1195,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(
@@ -1066,8 +1217,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);
+ });
+}
diff --git a/devtools/client/themes/storage.css b/devtools/client/themes/storage.css
index 1e611f842..1d4da9bd6 100644
--- a/devtools/client/themes/storage.css
+++ b/devtools/client/themes/storage.css
@@ -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,
diff --git a/devtools/server/actors/storage.js b/devtools/server/actors/storage.js
index 572cd6b68..015d849ee 100644
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -15,6 +15,17 @@ const {isWindowIncluded} = require("devtools/shared/layout/utils");
const specs = require("devtools/shared/specs/storage");
const { Task } = require("devtools/shared/task");
+const DEFAULT_VALUE = "value";
+
+loader.lazyRequireGetter(this, "naturalSortCaseInsensitive",
+ "devtools/client/shared/natural-sort", true);
+
+// 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
+// devtools/server/tests/browser/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
loader.lazyImporter(this, "Sqlite", "resource://gre/modules/Sqlite.jsm");
@@ -87,7 +98,7 @@ var StorageActors = {};
* - observe : Method which gets triggered on the notificaiton of the watched
* topic.
* - getNamesForHost : Given a host, get list of all known store names.
- * - getValuesForHost : Given a host (and optianally a name) get all known
+ * - getValuesForHost : Given a host (and optionally a name) get all known
* store objects.
* - toStoreObject : Given a store object, convert it to the required format
* so that it can be transferred over wire.
@@ -135,6 +146,9 @@ StorageActors.defaults = function (typeName, observationTopic) {
* Converts the window.location object into host.
*/
getHostName(location) {
+ if (location.protocol === "chrome:") {
+ return location.href;
+ }
return location.hostname || location.href;
},
@@ -315,15 +329,20 @@ StorageActors.defaults = function (typeName, observationTopic) {
toReturn.data.push(...values);
}
}
+
toReturn.total = this.getObjectsSize(host, names, options);
+
if (offset > toReturn.total) {
// In this case, toReturn.data is an empty array.
toReturn.offset = toReturn.total;
toReturn.data = [];
} else {
- toReturn.data = toReturn.data.sort((a, b) => {
- return a[sortOn] - b[sortOn];
- }).slice(offset, offset + size).map(a => this.toStoreObject(a));
+ // We need to use natural sort before slicing.
+ let sorted = toReturn.data.sort((a, b) => {
+ return naturalSortCaseInsensitive(a[sortOn], b[sortOn]);
+ });
+ let sliced = sorted.slice(offset, offset + size);
+ toReturn.data = sliced.map(a => this.toStoreObject(a));
}
} else {
let obj = yield this.getValuesForHost(host, undefined, undefined,
@@ -333,15 +352,18 @@ StorageActors.defaults = function (typeName, observationTopic) {
}
toReturn.total = obj.length;
+
if (offset > toReturn.total) {
// In this case, toReturn.data is an empty array.
toReturn.offset = offset = toReturn.total;
toReturn.data = [];
} else {
- toReturn.data = obj.sort((a, b) => {
- return a[sortOn] - b[sortOn];
- }).slice(offset, offset + size)
- .map(object => this.toStoreObject(object));
+ // We need to use natural sort before slicing.
+ let sorted = obj.sort((a, b) => {
+ return naturalSortCaseInsensitive(a[sortOn], b[sortOn]);
+ });
+ let sliced = sorted.slice(offset, offset + size);
+ toReturn.data = sliced.map(object => this.toStoreObject(object));
}
}
@@ -460,11 +482,13 @@ StorageActors.createActor({
}
return {
+ uniqueKey: `${cookie.name}${SEPARATOR_GUID}${cookie.host}` +
+ `${SEPARATOR_GUID}${cookie.path}`,
name: cookie.name,
- path: cookie.path || "",
host: cookie.host || "",
+ path: cookie.path || "",
- // because expires is in seconds
+ // because creationTime is in micro seconds
expires: (cookie.expires || 0) * 1000,
// because it is in micro seconds
@@ -488,7 +512,10 @@ StorageActors.createActor({
for (let cookie of cookies) {
if (this.isCookieAtHost(cookie, host)) {
- this.hostVsStores.get(host).set(cookie.name, cookie);
+ let uniqueKey = `${cookie.name}${SEPARATOR_GUID}${cookie.host}` +
+ `${SEPARATOR_GUID}${cookie.path}`;
+
+ this.hostVsStores.get(host).set(uniqueKey, cookie);
}
}
},
@@ -521,8 +548,11 @@ StorageActors.createActor({
case "changed":
if (hosts.length) {
for (let host of hosts) {
- this.hostVsStores.get(host).set(subject.name, subject);
- data[host] = [subject.name];
+ let uniqueKey = `${subject.name}${SEPARATOR_GUID}${subject.host}` +
+ `${SEPARATOR_GUID}${subject.path}`;
+
+ this.hostVsStores.get(host).set(uniqueKey, subject);
+ data[host] = [uniqueKey];
}
this.storageActor.update(action, "cookies", data);
}
@@ -531,8 +561,11 @@ StorageActors.createActor({
case "deleted":
if (hosts.length) {
for (let host of hosts) {
- this.hostVsStores.get(host).delete(subject.name);
- data[host] = [subject.name];
+ let uniqueKey = `${subject.name}${SEPARATOR_GUID}${subject.host}` +
+ `${SEPARATOR_GUID}${subject.path}`;
+
+ this.hostVsStores.get(host).delete(uniqueKey);
+ data[host] = [uniqueKey];
}
this.storageActor.update("deleted", "cookies", data);
}
@@ -543,8 +576,11 @@ StorageActors.createActor({
for (let host of hosts) {
let stores = [];
for (let cookie of subject) {
- this.hostVsStores.get(host).delete(cookie.name);
- stores.push(cookie.name);
+ let uniqueKey = `${cookie.name}${SEPARATOR_GUID}${cookie.host}` +
+ `${SEPARATOR_GUID}${cookie.path}`;
+
+ this.hostVsStores.get(host).delete(uniqueKey);
+ stores.push(uniqueKey);
}
data[host] = stores;
}
@@ -566,15 +602,17 @@ StorageActors.createActor({
getFields: Task.async(function* () {
return [
- { name: "name", editable: 1},
- { name: "path", editable: 1},
- { name: "host", editable: 1},
- { name: "expires", editable: 1},
- { name: "lastAccessed", editable: 0},
- { name: "value", editable: 1},
- { name: "isDomain", editable: 0},
- { name: "isSecure", editable: 1},
- { name: "isHttpOnly", editable: 1}
+ { name: "uniqueKey", editable: false, private: true },
+ { name: "name", editable: true, hidden: false },
+ { name: "host", editable: true, hidden: false },
+ { name: "path", editable: true, hidden: false },
+ { name: "expires", editable: true, hidden: false },
+ { name: "lastAccessed", editable: false, hidden: false },
+ { name: "creationTime", editable: false, hidden: true },
+ { name: "value", editable: true, hidden: false },
+ { name: "isDomain", editable: false, hidden: true },
+ { name: "isSecure", editable: true, hidden: true },
+ { name: "isHttpOnly", editable: true, hidden: false }
];
}),
@@ -591,6 +629,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
@@ -603,6 +649,12 @@ StorageActors.createActor({
.originAttributes);
}),
+ removeAllSessionCookies: Task.async(function* (host, domain) {
+ let doc = this.storageActor.document;
+ this.removeAllSessionCookies(host, domain, doc.nodePrincipal
+ .originAttributes);
+ }),
+
maybeSetupChildProcess() {
cookieHelpers.onCookieChanged = this.onCookieChanged.bind(this);
@@ -619,6 +671,8 @@ StorageActors.createActor({
cookieHelpers.removeCookie.bind(cookieHelpers);
this.removeAllCookies =
cookieHelpers.removeAllCookies.bind(cookieHelpers);
+ this.removeAllSessionCookies =
+ cookieHelpers.removeAllSessionCookies.bind(cookieHelpers);
return;
}
@@ -642,6 +696,8 @@ StorageActors.createActor({
callParentProcess.bind(null, "removeCookie");
this.removeAllCookies =
callParentProcess.bind(null, "removeAllCookies");
+ this.removeAllSessionCookies =
+ callParentProcess.bind(null, "removeAllSessionCookies");
addMessageListener("debug:storage-cookie-request-child",
cookieHelpers.handleParentRequest);
@@ -696,7 +752,7 @@ var cookieHelpers = {
* {
* host: "http://www.mozilla.org",
* field: "value",
- * key: "name",
+ * editCookie: "name",
* oldValue: "%7BHello%7D",
* newValue: "%7BHelloo%7D",
* items: {
@@ -720,10 +776,14 @@ var cookieHelpers = {
let origPath = field === "path" ? oldValue : data.items.path;
let cookie = null;
- let enumerator = Services.cookies.getCookiesFromHost(origHost, data.originAttributes || {});
+ let enumerator =
+ Services.cookies.getCookiesFromHost(origHost, data.originAttributes || {});
+
while (enumerator.hasMoreElements()) {
let nsiCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
- if (nsiCookie.name === origName && nsiCookie.host === origHost) {
+ if (nsiCookie.name === origName &&
+ nsiCookie.host === origHost &&
+ nsiCookie.path === origPath) {
cookie = {
host: nsiCookie.host,
path: nsiCookie.path,
@@ -743,7 +803,7 @@ var cookieHelpers = {
return;
}
- // If the date is expired set it for 1 minute in the future.
+ // If the date is expired set it for 10 seconds in the future.
let now = new Date();
if (!cookie.isSession && (cookie.expires * 1000) <= now) {
let tenSecondsFromNow = (now.getTime() + 10 * 1000) / 1000;
@@ -797,6 +857,15 @@ var cookieHelpers = {
},
_removeCookies(host, opts = {}) {
+ // We use a uniqueId to emulate compound keys for cookies. We need to
+ // extract the cookie name to remove the correct cookie.
+ if (opts.name) {
+ let split = opts.name.split(SEPARATOR_GUID);
+
+ opts.name = split[0];
+ opts.path = split[2];
+ }
+
function hostMatches(cookieHost, matchHost) {
if (cookieHost == null) {
return matchHost == null;
@@ -807,12 +876,16 @@ var cookieHelpers = {
return cookieHost == host;
}
- let enumerator = Services.cookies.getCookiesFromHost(host, opts.originAttributes || {});
+ let enumerator =
+ Services.cookies.getCookiesFromHost(host, opts.originAttributes || {});
+
while (enumerator.hasMoreElements()) {
let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
if (hostMatches(cookie.host, host) &&
(!opts.name || cookie.name === opts.name) &&
- (!opts.domain || cookie.host === opts.domain)) {
+ (!opts.domain || cookie.host === opts.domain) &&
+ (!opts.path || cookie.path === opts.path) &&
+ (!opts.session || (!cookie.expires && !cookie.maxAge))) {
Services.cookies.remove(
cookie.host,
cookie.name,
@@ -834,6 +907,10 @@ var cookieHelpers = {
this._removeCookies(host, { domain, originAttributes });
},
+ removeAllSessionCookies(host, domain, originAttributes) {
+ this._removeCookies(host, { domain, originAttributes, session: true });
+ },
+
addCookieObservers() {
Services.obs.addObserver(cookieHelpers, "cookie-changed", false);
return null;
@@ -898,6 +975,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];
@@ -910,6 +993,12 @@ var cookieHelpers = {
let originAttributes = msg.data.args[2];
return cookieHelpers.removeAllCookies(host, domain, originAttributes);
}
+ case "removeAllSessionCookies": {
+ let host = msg.data.args[0];
+ let domain = msg.data.args[1];
+ let originAttributes = msg.data.args[2];
+ return cookieHelpers.removeAllSessionCookies(host, domain, originAttributes);
+ }
default:
console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method);
throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD");
@@ -1004,6 +1093,9 @@ function getObjectForLocalOrSessionStorage(type) {
if (!location.host) {
return location.href;
}
+ if (location.protocol === "chrome:") {
+ return location.href;
+ }
return location.protocol + "//" + location.host;
},
@@ -1024,11 +1116,19 @@ function getObjectForLocalOrSessionStorage(type) {
getFields: Task.async(function* () {
return [
- { name: "name", editable: 1},
- { name: "value", editable: 1}
+ { name: "name", editable: true },
+ { name: "value", editable: true }
];
}),
+ 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.
*
@@ -1143,6 +1243,11 @@ StorageActors.createActor({
// The |chrome| cache is the cache implicitely cached by the platform,
// hosting the source file of the service worker.
let { CacheStorage } = this.storageActor.window;
+
+ if (!CacheStorage) {
+ return [];
+ }
+
let cache = new CacheStorage("content", principal);
return cache;
}),
@@ -1205,8 +1310,8 @@ StorageActors.createActor({
getFields: Task.async(function* () {
return [
- { name: "url", editable: 0 },
- { name: "status", editable: 0 }
+ { name: "url", editable: false },
+ { name: "status", editable: false }
];
}),
@@ -1214,6 +1319,9 @@ StorageActors.createActor({
if (!location.host) {
return location.href;
}
+ if (location.protocol === "chrome:") {
+ return location.href;
+ }
return location.protocol + "//" + location.host;
},
@@ -1386,12 +1494,15 @@ ObjectStoreMetadata.prototype = {
* The host associated with this indexed db.
* @param {IDBDatabase} db
* The particular indexed db.
+ * @param {String} storage
+ * Storage type, either "temporary", "default" or "persistent".
*/
-function DatabaseMetadata(origin, db) {
+function DatabaseMetadata(origin, db, storage) {
this._origin = origin;
this._name = db.name;
this._version = db.version;
this._objectStores = [];
+ this.storage = storage;
if (db.objectStoreNames.length) {
let transaction = db.transaction(db.objectStoreNames, "readonly");
@@ -1411,7 +1522,9 @@ DatabaseMetadata.prototype = {
toObject() {
return {
+ uniqueKey: `${this._name}${SEPARATOR_GUID}${this.storage}`,
name: this._name,
+ storage: this.storage,
origin: this._origin,
version: this._version,
objectStores: this._objectStores.size
@@ -1487,6 +1600,9 @@ StorageActors.createActor({
if (!location.host) {
return location.href;
}
+ if (location.protocol === "chrome:") {
+ return location.href;
+ }
return location.protocol + "//" + location.host;
},
@@ -1579,15 +1695,17 @@ StorageActors.createActor({
populateStoresForHost: Task.async(function* (host) {
let storeMap = new Map();
let {names} = yield this.getDBNamesForHost(host);
+
let win = this.storageActor.getWindowFromHost(host);
if (win) {
let principal = win.document.nodePrincipal;
- for (let name of names) {
- let metadata = yield this.getDBMetaData(host, principal, name);
+ for (let {name, storage} of names) {
+ let metadata = yield this.getDBMetaData(host, principal, name, storage);
metadata = indexedDBHelpers.patchMetadataMapsAndProtos(metadata);
- storeMap.set(name, metadata);
+
+ storeMap.set(`${name} (${storage})`, metadata);
}
}
@@ -1614,16 +1732,30 @@ StorageActors.createActor({
if ("objectStores" in item) {
// DB meta data
return {
+ uniqueKey: `${item.name} (${item.storage})`,
db: item.name,
+ storage: item.storage,
origin: item.origin,
version: item.version,
objectStores: item.objectStores
};
}
+
+ let value = JSON.stringify(item.value);
+
+ // FIXME: Bug 1318029 - Due to a bug that is thrown whenever a
+ // LongStringActor string reaches DebuggerServer.LONG_STRING_LENGTH we need
+ // to trim the value. When the bug is fixed we should stop trimming the
+ // string here.
+ let maxLength = DebuggerServer.LONG_STRING_LENGTH - 1;
+ if (value.length > maxLength) {
+ value = value.substr(0, maxLength);
+ }
+
// Indexed db entry
return {
name: item.name,
- value: new LongStringActor(this.conn, JSON.stringify(item.value))
+ value: new LongStringActor(this.conn, value)
};
},
@@ -1659,16 +1791,20 @@ StorageActors.createActor({
maybeSetupChildProcess() {
if (!DebuggerServer.isInChildProcess) {
this.backToChild = (func, rv) => rv;
+ this.clearDBStore = indexedDBHelpers.clearDBStore;
+ this.findIDBPathsForHost = indexedDBHelpers.findIDBPathsForHost;
+ this.findSqlitePathsForHost = indexedDBHelpers.findSqlitePathsForHost;
+ this.findStorageTypePaths = indexedDBHelpers.findStorageTypePaths;
this.getDBMetaData = indexedDBHelpers.getDBMetaData;
- this.openWithPrincipal = indexedDBHelpers.openWithPrincipal;
this.getDBNamesForHost = indexedDBHelpers.getDBNamesForHost;
- this.getSanitizedHost = indexedDBHelpers.getSanitizedHost;
this.getNameFromDatabaseFile = indexedDBHelpers.getNameFromDatabaseFile;
- this.getValuesForHost = indexedDBHelpers.getValuesForHost;
this.getObjectStoreData = indexedDBHelpers.getObjectStoreData;
+ this.getSanitizedHost = indexedDBHelpers.getSanitizedHost;
+ this.getValuesForHost = indexedDBHelpers.getValuesForHost;
+ this.openWithPrincipal = indexedDBHelpers.openWithPrincipal;
this.removeDB = indexedDBHelpers.removeDB;
this.removeDBRecord = indexedDBHelpers.removeDBRecord;
- this.clearDBStore = indexedDBHelpers.clearDBStore;
+ this.splitNameAndStorage = indexedDBHelpers.splitNameAndStorage;
return;
}
@@ -1681,6 +1817,7 @@ StorageActors.createActor({
});
this.getDBMetaData = callParentProcessAsync.bind(null, "getDBMetaData");
+ this.splitNameAndStorage = callParentProcessAsync.bind(null, "splitNameAndStorage");
this.getDBNamesForHost = callParentProcessAsync.bind(null, "getDBNamesForHost");
this.getValuesForHost = callParentProcessAsync.bind(null, "getValuesForHost");
this.removeDB = callParentProcessAsync.bind(null, "removeDB");
@@ -1725,26 +1862,28 @@ StorageActors.createActor({
// Detail of database
case "database":
return [
- { name: "objectStore", editable: 0 },
- { name: "keyPath", editable: 0 },
- { name: "autoIncrement", editable: 0 },
- { name: "indexes", editable: 0 },
+ { name: "objectStore", editable: false },
+ { name: "keyPath", editable: false },
+ { name: "autoIncrement", editable: false },
+ { name: "indexes", editable: false },
];
// Detail of object store
case "object store":
return [
- { name: "name", editable: 0 },
- { name: "value", editable: 0 }
+ { name: "name", editable: false },
+ { name: "value", editable: false }
];
// Detail of indexedDB for one origin
default:
return [
- { name: "db", editable: 0 },
- { name: "origin", editable: 0 },
- { name: "version", editable: 0 },
- { name: "objectStores", editable: 0 },
+ { name: "uniqueKey", editable: false, private: true },
+ { name: "db", editable: false },
+ { name: "storage", editable: false },
+ { name: "origin", editable: false },
+ { name: "version", editable: false },
+ { name: "objectStores", editable: false },
];
}
})
@@ -1776,14 +1915,14 @@ var indexedDBHelpers = {
* `name` for the given `host` with its `principal`. The stored metadata
* information is of `DatabaseMetadata` type.
*/
- getDBMetaData: Task.async(function* (host, principal, name) {
- let request = this.openWithPrincipal(principal, name);
+ getDBMetaData: Task.async(function* (host, principal, name, storage) {
+ let request = this.openWithPrincipal(principal, name, storage);
let success = promise.defer();
request.onsuccess = event => {
let db = event.target.result;
- let dbData = new DatabaseMetadata(host, db);
+ let dbData = new DatabaseMetadata(host, db, storage);
db.close();
success.resolve(this.backToChild("getDBMetaData", dbData));
@@ -1796,21 +1935,37 @@ var indexedDBHelpers = {
return success.promise;
}),
+ splitNameAndStorage: function (name) {
+ let lastOpenBracketIndex = name.lastIndexOf("(");
+ let lastCloseBracketIndex = name.lastIndexOf(")");
+ let delta = lastCloseBracketIndex - lastOpenBracketIndex - 1;
+
+ let storage = name.substr(lastOpenBracketIndex + 1, delta);
+
+ name = name.substr(0, lastOpenBracketIndex - 1);
+
+ return { storage, name };
+ },
+
/**
* Opens an indexed db connection for the given `principal` and
* database `name`.
*/
- openWithPrincipal(principal, name) {
- return indexedDBForStorage.openForPrincipal(principal, name);
+ openWithPrincipal: function (principal, name, storage) {
+ return indexedDBForStorage.openForPrincipal(principal, name,
+ { storage: storage });
},
- removeDB: Task.async(function* (host, principal, name) {
+ removeDB: Task.async(function* (host, principal, dbName) {
let result = new promise(resolve => {
- let request = indexedDBForStorage.deleteForPrincipal(principal, name);
+ let {name, storage} = this.splitNameAndStorage(dbName);
+ let request =
+ indexedDBForStorage.deleteForPrincipal(principal, name,
+ { storage: storage });
request.onsuccess = () => {
resolve({});
- this.onItemUpdated("deleted", host, [name]);
+ this.onItemUpdated("deleted", host, [dbName]);
};
request.onblocked = () => {
@@ -1836,10 +1991,11 @@ var indexedDBHelpers = {
removeDBRecord: Task.async(function* (host, principal, dbName, storeName, id) {
let db;
+ let {name, storage} = this.splitNameAndStorage(dbName);
try {
db = yield new promise((resolve, reject) => {
- let request = this.openWithPrincipal(principal, dbName);
+ let request = this.openWithPrincipal(principal, name, storage);
request.onsuccess = ev => resolve(ev.target.result);
request.onerror = ev => reject(ev.target.error);
});
@@ -1868,10 +2024,11 @@ var indexedDBHelpers = {
clearDBStore: Task.async(function* (host, principal, dbName, storeName) {
let db;
+ let {name, storage} = this.splitNameAndStorage(dbName);
try {
db = yield new promise((resolve, reject) => {
- let request = this.openWithPrincipal(principal, dbName);
+ let request = this.openWithPrincipal(principal, name, storage);
request.onsuccess = ev => resolve(ev.target.result);
request.onerror = ev => reject(ev.target.error);
});
@@ -1903,46 +2060,101 @@ var indexedDBHelpers = {
*/
getDBNamesForHost: Task.async(function* (host) {
let sanitizedHost = this.getSanitizedHost(host);
- let directory = OS.Path.join(OS.Constants.Path.profileDir, "storage",
- "default", sanitizedHost, "idb");
-
- let exists = yield OS.File.exists(directory);
- if (!exists && host.startsWith("about:")) {
- // try for moz-safe-about directory
- sanitizedHost = this.getSanitizedHost("moz-safe-" + host);
- directory = OS.Path.join(OS.Constants.Path.profileDir, "storage",
- "permanent", sanitizedHost, "idb");
- exists = yield OS.File.exists(directory);
- }
- if (!exists) {
- return this.backToChild("getDBNamesForHost", {names: []});
+ let profileDir = OS.Constants.Path.profileDir;
+ let files = [];
+ let names = [];
+ let storagePath = OS.Path.join(profileDir, "storage");
+
+ // We expect sqlite DB paths to look something like this:
+ // - PathToProfileDir/storage/default/http+++www.example.com/
+ // idb/1556056096MeysDaabta.sqlite
+ // - PathToProfileDir/storage/permanent/http+++www.example.com/
+ // idb/1556056096MeysDaabta.sqlite
+ // - PathToProfileDir/storage/temporary/http+++www.example.com/
+ // idb/1556056096MeysDaabta.sqlite
+ // The subdirectory inside the storage folder is determined by the storage
+ // type:
+ // - default: { storage: "default" } or not specified.
+ // - permanent: { storage: "persistent" }.
+ // - temporary: { storage: "temporary" }.
+ let sqliteFiles = yield this.findSqlitePathsForHost(storagePath, sanitizedHost);
+
+ for (let file of sqliteFiles) {
+ let splitPath = OS.Path.split(file).components;
+ let idbIndex = splitPath.indexOf("idb");
+ let storage = splitPath[idbIndex - 2];
+ let relative = file.substr(profileDir.length + 1);
+
+ files.push({
+ file: relative,
+ storage: storage === "permanent" ? "persistent" : storage
+ });
}
- let names = [];
- let dirIterator = new OS.File.DirectoryIterator(directory);
- try {
- yield dirIterator.forEach(file => {
- // Skip directories.
- if (file.isDir) {
- return null;
+ if (files.length > 0) {
+ for (let {file, storage} of files) {
+ let name = yield this.getNameFromDatabaseFile(file);
+ if (name) {
+ names.push({
+ name,
+ storage
+ });
}
+ }
+ }
+ return this.backToChild("getDBNamesForHost", {names});
+ }),
- // Skip any non-sqlite files.
- if (!file.name.endsWith(".sqlite")) {
- return null;
+ /**
+ * Find all SQLite files that hold IndexedDB data for a host, such as:
+ * storage/temporary/http+++www.example.com/idb/1556056096MeysDaabta.sqlite
+ */
+ findSqlitePathsForHost: Task.async(function* (storagePath, sanitizedHost) {
+ let sqlitePaths = [];
+ let idbPaths = yield this.findIDBPathsForHost(storagePath, sanitizedHost);
+ for (let idbPath of idbPaths) {
+ let iterator = new OS.File.DirectoryIterator(idbPath);
+ yield iterator.forEach(entry => {
+ if (!entry.isDir && entry.path.endsWith(".sqlite")) {
+ sqlitePaths.push(entry.path);
}
-
- return this.getNameFromDatabaseFile(file.path).then(name => {
- if (name) {
- names.push(name);
- }
- return null;
- });
});
- } finally {
- dirIterator.close();
+ iterator.close();
}
- return this.backToChild("getDBNamesForHost", {names: names});
+ return sqlitePaths;
+ }),
+
+ /**
+ * Find all paths that hold IndexedDB data for a host, such as:
+ * storage/temporary/http+++www.example.com/idb
+ */
+ findIDBPathsForHost: Task.async(function* (storagePath, sanitizedHost) {
+ let idbPaths = [];
+ let typePaths = yield this.findStorageTypePaths(storagePath);
+ for (let typePath of typePaths) {
+ let idbPath = OS.Path.join(typePath, sanitizedHost, "idb");
+ if (yield OS.File.exists(idbPath)) {
+ idbPaths.push(idbPath);
+ }
+ }
+ return idbPaths;
+ }),
+
+ /**
+ * Find all the storage types, such as "default", "permanent", or "temporary".
+ * These names have changed over time, so it seems simpler to look through all types
+ * that currently exist in the profile.
+ */
+ findStorageTypePaths: Task.async(function* (storagePath) {
+ let iterator = new OS.File.DirectoryIterator(storagePath);
+ let typePaths = [];
+ yield iterator.forEach(entry => {
+ if (entry.isDir) {
+ typePaths.push(entry.path);
+ }
+ });
+ iterator.close();
+ return typePaths;
}),
/**
@@ -1950,6 +2162,9 @@ var indexedDBHelpers = {
* name.
*/
getSanitizedHost(host) {
+ if (host.startsWith("about:")) {
+ host = "moz-safe-" + host;
+ }
return host.replace(ILLEGAL_CHAR_REGEX, "+");
},
@@ -1963,7 +2178,7 @@ var indexedDBHelpers = {
// Content pages might be having an open transaction for the same indexed db
// which this sqlite file belongs to. In that case, sqlite.openConnection
- // will throw. Thus we retey for some time to see if lock is removed.
+ // will throw. Thus we retry for some time to see if lock is removed.
while (!connection && retryCount++ < 25) {
try {
connection = yield Sqlite.openConnection({ path: path });
@@ -2024,8 +2239,14 @@ var indexedDBHelpers = {
return this.backToChild("getValuesForHost", {objectStores: objectStores});
}
// Get either all entries from the object store, or a particular id
- let result = yield this.getObjectStoreData(host, principal, db2,
- objectStore, id, options.index, options.size);
+ let storage = hostVsStores.get(host).get(db2).storage;
+ let result = yield this.getObjectStoreData(host, principal, db2, storage, {
+ objectStore: objectStore,
+ id: id,
+ index: options.index,
+ offset: 0,
+ size: options.size
+ });
return this.backToChild("getValuesForHost", {result: result});
}),
@@ -2039,23 +2260,27 @@ var indexedDBHelpers = {
* The principal of the given document.
* @param {string} dbName
* The name of the indexed db from the above host.
- * @param {string} objectStore
- * The name of the object store from the above db.
- * @param {string} id
- * id of the requested entry from the above object store.
- * null if all entries from the above object store are requested.
- * @param {string} index
- * name of the IDBIndex to be iterated on while fetching entries.
- * null or "name" if no index is to be iterated.
- * @param {number} offset
- * ofsset of the entries to be fetched.
- * @param {number} size
- * The intended size of the entries to be fetched.
+ * @param {String} storage
+ * Storage type, either "temporary", "default" or "persistent".
+ * @param {Object} requestOptions
+ * An object in the following format:
+ * {
+ * objectStore: The name of the object store from the above db,
+ * id: Id of the requested entry from the above object
+ * store. null if all entries from the above object
+ * store are requested,
+ * index: Name of the IDBIndex to be iterated on while fetching
+ * entries. null or "name" if no index is to be
+ * iterated,
+ * offset: offset of the entries to be fetched,
+ * size: The intended size of the entries to be fetched
+ * }
*/
- getObjectStoreData(host, principal, dbName, objectStore, id, index,
- offset, size) {
- let request = this.openWithPrincipal(principal, dbName);
+ getObjectStoreData(host, principal, dbName, storage, requestOptions) {
+ let {name} = this.splitNameAndStorage(dbName);
+ let request = this.openWithPrincipal(principal, name, storage);
let success = promise.defer();
+ let {objectStore, id, index, offset, size} = requestOptions;
let data = [];
let db;
@@ -2157,8 +2382,12 @@ var indexedDBHelpers = {
switch (msg.json.method) {
case "getDBMetaData": {
- let [host, principal, name] = args;
- return indexedDBHelpers.getDBMetaData(host, principal, name);
+ let [host, principal, name, storage] = args;
+ return indexedDBHelpers.getDBMetaData(host, principal, name, storage);
+ }
+ case "splitNameAndStorage": {
+ let [name] = args;
+ return indexedDBHelpers.splitNameAndStorage(name);
}
case "getDBNamesForHost": {
let [host] = args;
@@ -2170,8 +2399,8 @@ var indexedDBHelpers = {
hostVsStores, principal);
}
case "removeDB": {
- let [host, principal, name] = args;
- return indexedDBHelpers.removeDB(host, principal, name);
+ let [host, principal, dbName] = args;
+ return indexedDBHelpers.removeDB(host, principal, dbName);
}
case "removeDBRecord": {
let [host, principal, db, store, id] = args;
@@ -2480,6 +2709,7 @@ let StorageActor = protocol.ActorClassWithSpec(specs.storageSpec, {
// added or changed update
this.removeNamesFromUpdateList("added", storeType, data);
this.removeNamesFromUpdateList("changed", storeType, data);
+
for (let host in data) {
if (data[host].length == 0 && this.boundUpdate.added &&
this.boundUpdate.added[storeType] &&
diff --git a/devtools/server/tests/browser/browser.ini b/devtools/server/tests/browser/browser.ini
index c05933230..b7929e2b0 100644
--- a/devtools/server/tests/browser/browser.ini
+++ b/devtools/server/tests/browser/browser.ini
@@ -11,6 +11,7 @@ support-files =
doc_perf.html
navigate-first.html
navigate-second.html
+ storage-cookies-same-name.html
storage-dynamic-windows.html
storage-listings.html
storage-unsecured-iframe.html
@@ -80,6 +81,7 @@ skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still di
#[browser_perf-front-profiler-01.js] bug 1077464
#[browser_perf-front-profiler-05.js] bug 1077464
#[browser_perf-front-profiler-06.js]
+[browser_storage_cookies-duplicate-names.js]
[browser_storage_dynamic_windows.js]
[browser_storage_listings.js]
[browser_storage_updates.js]
diff --git a/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js b/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js
new file mode 100644
index 000000000..c1cf0aa72
--- /dev/null
+++ b/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js
@@ -0,0 +1,105 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the storage panel is able to display multiple cookies with the same
+// name (and different paths).
+
+const {StorageFront} = require("devtools/shared/fronts/storage");
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js", this);
+
+const TESTDATA = {
+ "test1.example.org": [
+ {
+ name: "name",
+ value: "value1",
+ expires: 0,
+ path: "/",
+ host: "test1.example.org",
+ isDomain: false,
+ isSecure: false,
+ },
+ {
+ name: "name",
+ value: "value2",
+ expires: 0,
+ path: "/path2/",
+ host: "test1.example.org",
+ isDomain: false,
+ isSecure: false,
+ },
+ {
+ name: "name",
+ value: "value3",
+ expires: 0,
+ path: "/path3/",
+ host: "test1.example.org",
+ isDomain: false,
+ isSecure: false,
+ }
+ ]
+};
+
+add_task(function* () {
+ yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies-same-name.html");
+
+ initDebuggerServer();
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ let form = yield connectDebuggerClient(client);
+ let front = StorageFront(client, form);
+ let data = yield front.listStores();
+
+ ok(data.cookies, "Cookies storage actor is present");
+
+ yield testCookies(data.cookies);
+ yield clearStorage();
+
+ // Forcing GC/CC to get rid of docshells and windows created by this test.
+ forceCollections();
+ yield client.close();
+ forceCollections();
+ DebuggerServer.destroy();
+ forceCollections();
+});
+
+function testCookies(cookiesActor) {
+ let numHosts = Object.keys(cookiesActor.hosts).length;
+ is(numHosts, 1, "Correct number of host entries for cookies");
+ return testCookiesObjects(0, cookiesActor.hosts, cookiesActor);
+}
+
+var testCookiesObjects = Task.async(function* (index, hosts, cookiesActor) {
+ let host = Object.keys(hosts)[index];
+ let matchItems = data => {
+ is(data.total, TESTDATA[host].length,
+ "Number of cookies in host " + host + " matches");
+ for (let item of data.data) {
+ let found = false;
+ for (let toMatch of TESTDATA[host]) {
+ if (item.name === toMatch.name &&
+ item.host === toMatch.host &&
+ item.path === toMatch.path) {
+ found = true;
+ ok(true, "Found cookie " + item.name + " in response");
+ is(item.value.str, toMatch.value, "The value matches.");
+ is(item.expires, toMatch.expires, "The expiry time matches.");
+ is(item.path, toMatch.path, "The path matches.");
+ is(item.host, toMatch.host, "The host matches.");
+ is(item.isSecure, toMatch.isSecure, "The isSecure value matches.");
+ is(item.isDomain, toMatch.isDomain, "The isDomain value matches.");
+ break;
+ }
+ }
+ ok(found, "cookie " + item.name + " should exist in response");
+ }
+ };
+
+ ok(!!TESTDATA[host], "Host is present in the list : " + host);
+ matchItems(yield cookiesActor.getStoreObjects(host));
+ if (index == Object.keys(hosts).length - 1) {
+ return;
+ }
+ yield testCookiesObjects(++index, hosts, cookiesActor);
+});
diff --git a/devtools/server/tests/browser/browser_storage_dynamic_windows.js b/devtools/server/tests/browser/browser_storage_dynamic_windows.js
index 440c91222..91b4155d9 100644
--- a/devtools/server/tests/browser/browser_storage_dynamic_windows.js
+++ b/devtools/server/tests/browser/browser_storage_dynamic_windows.js
@@ -66,6 +66,7 @@ function markOutMatched(toBeEmptied, data, deleted) {
info("Testing for " + storageType);
for (let host in data[storageType]) {
ok(toBeEmptied[storageType][host], "Host " + host + " found");
+
if (!deleted) {
for (let item of data[storageType][host]) {
let index = toBeEmptied[storageType][host].indexOf(item);
@@ -87,50 +88,6 @@ function markOutMatched(toBeEmptied, data, deleted) {
}
}
-// function testReload(front) {
-// info("Testing if reload works properly");
-
-// let shouldBeEmptyFirst = Cu.cloneInto(beforeReload, {});
-// let shouldBeEmptyLast = Cu.cloneInto(beforeReload, {});
-// return new Promise(resolve => {
-
-// let onStoresUpdate = data => {
-// info("in stores update of testReload");
-// // This might be second time stores update is happening, in which case,
-// // data.deleted will be null.
-// // OR.. This might be the first time on a super slow machine where both
-// // data.deleted and data.added is missing in the first update.
-// if (data.deleted) {
-// markOutMatched(shouldBeEmptyFirst, data.deleted, true);
-// }
-
-// if (!Object.keys(shouldBeEmptyFirst).length) {
-// info("shouldBeEmptyFirst is empty now");
-// }
-
-// // stores-update call might not have data.added for the first time on
-// // slow machines, in which case, data.added will be null
-// if (data.added) {
-// markOutMatched(shouldBeEmptyLast, data.added);
-// }
-
-// if (!Object.keys(shouldBeEmptyLast).length) {
-// info("Everything to be received is received.");
-// endTestReloaded();
-// }
-// };
-
-// let endTestReloaded = () => {
-// front.off("stores-update", onStoresUpdate);
-// resolve();
-// };
-
-// front.on("stores-update", onStoresUpdate);
-
-// content.location.reload();
-// });
-// }
-
function testAddIframe(front) {
info("Testing if new iframe addition works properly");
return new Promise(resolve => {
@@ -142,7 +99,10 @@ function testAddIframe(front) {
"https://sectest1.example.org": ["iframe-s-ss1"]
},
cookies: {
- "sectest1.example.org": ["sc1"]
+ "sectest1.example.org": [
+ getCookieId("sc1", "sectest1.example.org",
+ "/browser/devtools/server/tests/browser/")
+ ]
},
indexedDB: {
// empty because indexed db creation happens after the page load, so at
@@ -150,7 +110,7 @@ function testAddIframe(front) {
"https://sectest1.example.org": []
},
Cache: {
- "https://sectest1.example.org":[]
+ "https://sectest1.example.org": []
}
};
diff --git a/devtools/server/tests/browser/browser_storage_listings.js b/devtools/server/tests/browser/browser_storage_listings.js
index 4ff3c3fc1..e47a320b8 100644
--- a/devtools/server/tests/browser/browser_storage_listings.js
+++ b/devtools/server/tests/browser/browser_storage_listings.js
@@ -20,15 +20,6 @@ const storeMap = {
isSecure: false,
},
{
- name: "cs2",
- value: "sessionCookie",
- path: "/",
- host: ".example.org",
- expires: 0,
- isDomain: true,
- isSecure: false,
- },
- {
name: "c3",
value: "foobar-2",
expires: 2000000001000,
@@ -130,24 +121,24 @@ const storeMap = {
const IDBValues = {
listStoresResponse: {
"http://test1.example.org": [
- ["idb1", "obj1"], ["idb1", "obj2"], ["idb2", "obj3"]
+ ["idb1 (default)", "obj1"], ["idb1 (default)", "obj2"], ["idb2 (default)", "obj3"]
],
"http://sectest1.example.org": [
],
"https://sectest1.example.org": [
- ["idb-s1", "obj-s1"], ["idb-s2", "obj-s2"]
+ ["idb-s1 (default)", "obj-s1"], ["idb-s2 (default)", "obj-s2"]
]
},
- dbDetails : {
+ dbDetails: {
"http://test1.example.org": [
{
- db: "idb1",
+ db: "idb1 (default)",
origin: "http://test1.example.org",
version: 1,
objectStores: 2
},
{
- db: "idb2",
+ db: "idb2 (default)",
origin: "http://test1.example.org",
version: 1,
objectStores: 1
@@ -157,13 +148,13 @@ const IDBValues = {
],
"https://sectest1.example.org": [
{
- db: "idb-s1",
+ db: "idb-s1 (default)",
origin: "https://sectest1.example.org",
version: 1,
objectStores: 1
},
{
- db: "idb-s2",
+ db: "idb-s2 (default)",
origin: "https://sectest1.example.org",
version: 1,
objectStores: 1
@@ -172,7 +163,7 @@ const IDBValues = {
},
objectStoreDetails: {
"http://test1.example.org": {
- idb1: [
+ "idb1 (default)": [
{
objectStore: "obj1",
keyPath: "id",
@@ -199,7 +190,7 @@ const IDBValues = {
indexes: []
}
],
- idb2: [
+ "idb2 (default)": [
{
objectStore: "obj3",
keyPath: "id3",
@@ -217,7 +208,7 @@ const IDBValues = {
},
"http://sectest1.example.org" : {},
"https://sectest1.example.org": {
- "idb-s1": [
+ "idb-s1 (default)": [
{
objectStore: "obj-s1",
keyPath: "id",
@@ -225,7 +216,7 @@ const IDBValues = {
indexes: []
},
],
- "idb-s2": [
+ "idb-s2 (default)": [
{
objectStore: "obj-s2",
keyPath: "id3",
@@ -245,7 +236,7 @@ const IDBValues = {
},
entries: {
"http://test1.example.org": {
- "idb1#obj1": [
+ "idb1 (default)#obj1": [
{
name: 1,
value: {
@@ -271,7 +262,7 @@ const IDBValues = {
}
}
],
- "idb1#obj2": [
+ "idb1 (default)#obj2": [
{
name: 1,
value: {
@@ -282,11 +273,11 @@ const IDBValues = {
}
}
],
- "idb2#obj3": []
+ "idb2 (default)#obj3": []
},
"http://sectest1.example.org" : {},
"https://sectest1.example.org": {
- "idb-s1#obj-s1": [
+ "idb-s1 (default)#obj-s1": [
{
name: 6,
value: {
@@ -304,7 +295,7 @@ const IDBValues = {
}
}
],
- "idb-s2#obj-s2": [
+ "idb-s2 (default)#obj-s2": [
{
name: 13,
value: {
@@ -337,7 +328,8 @@ function* testStores(data) {
}
function testCookies(cookiesActor) {
- is(Object.keys(cookiesActor.hosts).length, 2, "Correct number of host entries for cookies");
+ is(Object.keys(cookiesActor.hosts).length, 2,
+ "Correct number of host entries for cookies");
return testCookiesObjects(0, cookiesActor.hosts, cookiesActor);
}
@@ -346,9 +338,9 @@ var testCookiesObjects = Task.async(function* (index, hosts, cookiesActor) {
let matchItems = data => {
let cookiesLength = 0;
for (let secureCookie of storeMap.cookies[host]) {
- if (secureCookie.isSecure) {
- ++cookiesLength;
- }
+ if (secureCookie.isSecure) {
+ ++cookiesLength;
+ }
}
// Any secure cookies did not get stored in the database.
is(data.total, storeMap.cookies[host].length - cookiesLength,
@@ -478,17 +470,17 @@ var testIndexedDBs = Task.async(function* (index, hosts, indexedDBActor) {
for (let item of data.data) {
let found = false;
for (let toMatch of IDBValues.dbDetails[host]) {
- if (item.db == toMatch.db) {
+ if (item.uniqueKey == toMatch.db) {
found = true;
- ok(true, "Found indexed db " + item.db + " in response");
+ ok(true, "Found indexed db " + item.uniqueKey + " in response");
is(item.origin, toMatch.origin, "The origin matches.");
is(item.version, toMatch.version, "The version matches.");
is(item.objectStores, toMatch.objectStores,
- "The numebr of object stores matches.");
+ "The number of object stores matches.");
break;
}
}
- ok(found, "indexed db " + item.name + " should exist in response");
+ ok(found, "indexed db " + item.uniqueKey + " should exist in response");
}
};
diff --git a/devtools/server/tests/browser/browser_storage_updates.js b/devtools/server/tests/browser/browser_storage_updates.js
index 28b2e509f..01a35cefc 100644
--- a/devtools/server/tests/browser/browser_storage_updates.js
+++ b/devtools/server/tests/browser/browser_storage_updates.js
@@ -27,7 +27,12 @@ const TESTS = [
expected: {
added: {
cookies: {
- "test1.example.org": ["c1", "c2"]
+ "test1.example.org": [
+ getCookieId("c1", "test1.example.org",
+ "/browser/devtools/server/tests/browser/"),
+ getCookieId("c2", "test1.example.org",
+ "/browser/devtools/server/tests/browser/")
+ ]
},
localStorage: {
"http://test1.example.org": ["l1"]
@@ -48,7 +53,10 @@ const TESTS = [
expected: {
changed: {
cookies: {
- "test1.example.org": ["c1"]
+ "test1.example.org": [
+ getCookieId("c1", "test1.example.org",
+ "/browser/devtools/server/tests/browser/"),
+ ]
}
},
added: {
@@ -74,7 +82,10 @@ const TESTS = [
expected: {
deleted: {
cookies: {
- "test1.example.org": ["c2"]
+ "test1.example.org": [
+ getCookieId("c2", "test1.example.org",
+ "/browser/devtools/server/tests/browser/"),
+ ]
},
localStorage: {
"http://test1.example.org": ["l1"]
@@ -112,7 +123,10 @@ const TESTS = [
expected: {
added: {
cookies: {
- "test1.example.org": ["c3"]
+ "test1.example.org": [
+ getCookieId("c3", "test1.example.org",
+ "/browser/devtools/server/tests/browser/"),
+ ]
},
sessionStorage: {
"http://test1.example.org": ["s1", "s2"]
@@ -125,7 +139,10 @@ const TESTS = [
},
deleted: {
cookies: {
- "test1.example.org": ["c1"]
+ "test1.example.org": [
+ getCookieId("c1", "test1.example.org",
+ "/browser/devtools/server/tests/browser/"),
+ ]
},
localStorage: {
"http://test1.example.org": ["l2"]
@@ -158,7 +175,10 @@ const TESTS = [
expected: {
deleted: {
cookies: {
- "test1.example.org": ["c3"]
+ "test1.example.org": [
+ getCookieId("c3", "test1.example.org",
+ "/browser/devtools/server/tests/browser/"),
+ ]
}
}
}
diff --git a/devtools/server/tests/browser/head.js b/devtools/server/tests/browser/head.js
index 1e7f09d95..5cf98c2b0 100644
--- a/devtools/server/tests/browser/head.js
+++ b/devtools/server/tests/browser/head.js
@@ -2,6 +2,10 @@
* 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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
@@ -19,6 +23,11 @@ const MAIN_DOMAIN = "http://test1.example.org/" + PATH;
const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH;
+// GUID to be used as a separator in compound keys. This must match the same
+// constant in devtools/server/actors/storage.js,
+// devtools/client/storage/ui.js and devtools/client/storage/test/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
// All tests are asynchronous.
waitForExplicitFinish();
@@ -94,7 +103,6 @@ function once(target, eventName, useCapture = false) {
info("Waiting for event: '" + eventName + "' on " + target + ".");
return new Promise(resolve => {
-
for (let [add, remove] of [
["addEventListener", "removeEventListener"],
["addListener", "removeListener"],
@@ -137,6 +145,8 @@ function getMockTabActor(win) {
}
registerCleanupFunction(function tearDown() {
+ Services.cookies.removeAll();
+
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
@@ -148,8 +158,11 @@ function idleWait(time) {
function busyWait(time) {
let start = Date.now();
+ // eslint-disable-next-line
let stack;
- while (Date.now() - start < time) { stack = Components.stack; }
+ while (Date.now() - start < time) {
+ stack = Components.stack;
+ }
}
/**
@@ -172,11 +185,12 @@ function waitUntil(predicate, interval = 10) {
}
function waitForMarkerType(front, types, predicate,
- unpackFun = (name, data) => data.markers,
- eventName = "timeline-data")
-{
+ unpackFun = (name, data) => data.markers,
+ eventName = "timeline-data") {
types = [].concat(types);
- predicate = predicate || function () { return true; };
+ predicate = predicate || function () {
+ return true;
+ };
let filteredMarkers = [];
let { promise, resolve } = defer();
@@ -190,9 +204,11 @@ function waitForMarkerType(front, types, predicate,
let markers = unpackFun(name, data);
info("Got markers: " + JSON.stringify(markers, null, 2));
- filteredMarkers = filteredMarkers.concat(markers.filter(m => types.indexOf(m.name) !== -1));
+ filteredMarkers = filteredMarkers.concat(
+ markers.filter(m => types.indexOf(m.name) !== -1));
- if (types.every(t => filteredMarkers.some(m => m.name === t)) && predicate(filteredMarkers)) {
+ if (types.every(t => filteredMarkers.some(m => m.name === t)) &&
+ predicate(filteredMarkers)) {
front.off(eventName, handler);
resolve(filteredMarkers);
}
@@ -201,3 +217,7 @@ function waitForMarkerType(front, types, predicate,
return promise;
}
+
+function getCookieId(name, domain, path) {
+ return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`;
+}
diff --git a/devtools/server/tests/browser/storage-cookies-same-name.html b/devtools/server/tests/browser/storage-cookies-same-name.html
new file mode 100644
index 000000000..e3e092ec3
--- /dev/null
+++ b/devtools/server/tests/browser/storage-cookies-same-name.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector cookies with duplicate names</title>
+</head>
+<body onload="createCookies()">
+<script type="application/javascript;version=1.7">
+"use strict";
+function createCookies() {
+ document.cookie = "name=value1;path=/;";
+ document.cookie = "name=value2;path=/path2/;";
+ document.cookie = "name=value3;path=/path3/;";
+}
+
+window.removeCookie = function (name) {
+ document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+};
+
+window.clearCookies = function () {
+ let cookies = document.cookie;
+ for (let cookie of cookies.split(";")) {
+ removeCookie(cookie.split("=")[0]);
+ }
+};
+</script>
+</body>
+</html>
diff --git a/devtools/shared/specs/storage.js b/devtools/shared/specs/storage.js
index d6ddaefe5..42a35073a 100644
--- a/devtools/shared/specs/storage.js
+++ b/devtools/shared/specs/storage.js
@@ -40,6 +40,7 @@ function createStorageSpec(options) {
// Cookies store object
types.addDictType("cookieobject", {
+ uniqueKey: "string",
name: "string",
value: "longstring",
path: "nullable:string",
@@ -61,7 +62,7 @@ types.addDictType("cookiestoreobject", {
// Common methods for edit/remove
const editRemoveMethods = {
- getEditableFields: {
+ getFields: {
request: {},
response: {
value: RetVal("json")
@@ -89,6 +90,13 @@ createStorageSpec({
methods: Object.assign({},
editRemoveMethods,
{
+ addItem: {
+ request: {
+ guid: Arg(0, "string"),
+ },
+ response: {}
+ }
+ }, {
removeAll: {
request: {
host: Arg(0, "string"),
@@ -96,6 +104,14 @@ createStorageSpec({
},
response: {}
}
+ }, {
+ removeAllSessionCookies: {
+ request: {
+ host: Arg(0, "string"),
+ domain: Arg(1, "nullable:string")
+ },
+ response: {}
+ }
}
)
});
@@ -110,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")
@@ -176,11 +200,13 @@ createStorageSpec({
// This is a union on idb object, db metadata object and object store metadata
// object
types.addDictType("idbobject", {
+ uniqueKey: "string",
name: "nullable:string",
db: "nullable:string",
objectStore: "nullable:string",
origin: "nullable:string",
version: "nullable:number",
+ storage: "nullable:string",
objectStores: "nullable:number",
keyPath: "nullable:string",
autoIncrement: "nullable:boolean",
diff --git a/toolkit/content/license.html b/toolkit/content/license.html
index 2e7982446..99ee42fde 100644
--- a/toolkit/content/license.html
+++ b/toolkit/content/license.html
@@ -121,6 +121,7 @@
<li><a href="about:license#hunspell-lt">Lithuanian Spellchecking Dictionary License</a></li>
<li><a href="about:license#microformatsshiv">MIT license — microformat-shiv</a></li>
<li><a href="about:license#myspell">MySpell License</a></li>
+ <li><a href="about:license#naturalSort">naturalSort License</a></li>
<li><a href="about:license#nicer">nICEr License</a></li>
<li><a href="about:license#node-properties">node-properties License</a></li>
<li><a href="about:license#nrappkit">nrappkit License</a></li>
@@ -4066,6 +4067,36 @@ SUCH DAMAGE.
</pre>
+<hr>
+
+<h1><a id="naturalSort"></a>naturalSort License</h1>
+
+<p>This license applies to <span class="path">devtools/client/shared/natural-sort.js</span>.</p>
+
+<pre>
+The MIT License (MIT)
+
+Copyright (c) 2014 Gabriel Llamas
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+</pre>
+
<hr>
<h1><a id="nicer"></a>nICEr License</h1>