--- a/devtools/client/netmonitor/src/components/request-list-header.js
+++ b/devtools/client/netmonitor/src/components/request-list-header.js
@@ -8,37 +8,26 @@ const {
createClass,
PropTypes,
DOM,
} = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const Actions = require("../actions/index");
const { getWaterfallScale } = require("../selectors/index");
const { getFormattedTime } = require("../utils/format-utils");
+const { HEADERS } = require("../constants");
const { L10N } = require("../utils/l10n");
const WaterfallBackground = require("../waterfall-background");
const RequestListHeaderContextMenu = require("../request-list-header-context-menu");
const { div, button } = DOM;
const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
-const HEADERS = [
- { name: "status", label: "status3" },
- { name: "method" },
- { name: "file", boxName: "icon-and-file" },
- { name: "domain", boxName: "security-and-domain" },
- { name: "cause" },
- { name: "type" },
- { name: "transferred" },
- { name: "contentSize", boxName: "size" },
- { name: "waterfall" }
-];
-
/**
* Render the request list header with sorting arrows for columns.
* Displays tick marks in the waterfall column header.
* Also draws the waterfall background canvas and updates it when needed.
*/
const RequestListHeader = createClass({
displayName: "RequestListHeader",
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -105,18 +105,64 @@ const EVENTS = {
PRIMED_CACHE_CHART_DISPLAYED: "NetMonitor:PrimedChartsDisplayed",
EMPTY_CACHE_CHART_DISPLAYED: "NetMonitor:EmptyChartsDisplayed",
// Fired once the NetMonitorController establishes a connection to the debug
// target.
CONNECTED: "connected",
};
+const HEADERS = [
+ {
+ name: "status",
+ label: "status3",
+ canFilter: true,
+ filterKey: "status-code"
+ },
+ {
+ name: "method",
+ canFilter: true,
+ },
+ {
+ name: "file",
+ boxName: "icon-and-file",
+ canFilter: false,
+ },
+ {
+ name: "domain",
+ boxName: "security-and-domain",
+ canFilter: true,
+ },
+ {
+ name: "cause",
+ canFilter: true,
+ },
+ {
+ name: "type",
+ canFilter: false,
+ },
+ {
+ name: "transferred",
+ canFilter: true,
+ },
+ {
+ name: "contentSize",
+ boxName: "size",
+ filterKey: "size",
+ canFilter: true,
+ },
+ {
+ name: "waterfall",
+ canFilter: false,
+ }
+];
+
const general = {
ACTIVITY_TYPE,
EVENTS,
FILTER_SEARCH_DELAY: 200,
+ HEADERS,
// 100 KB in bytes
SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE: 102400,
};
// flatten constants
module.exports = Object.assign({}, general, actionTypes);
--- a/devtools/client/netmonitor/src/request-list-header-context-menu.js
+++ b/devtools/client/netmonitor/src/request-list-header-context-menu.js
@@ -1,21 +1,22 @@
/* 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 Menu = require("devtools/client/framework/menu");
const MenuItem = require("devtools/client/framework/menu-item");
+const { HEADERS } = require("./constants");
const { L10N } = require("./utils/l10n");
-const stringMap = {
- status: "status3"
-};
+const stringMap = HEADERS
+ .filter((header) => header.hasOwnProperty("label"))
+ .reduce((acc, { name, label }) => Object.assign(acc, { [name]: label }), {});
class RequestListHeaderContextMenu {
constructor({ toggleColumn, resetColumns }) {
this.toggleColumn = toggleColumn;
this.resetColumns = resetColumns;
}
get columns() {
--- a/devtools/client/netmonitor/src/utils/filter-predicates.js
+++ b/devtools/client/netmonitor/src/utils/filter-predicates.js
@@ -1,14 +1,16 @@
/* 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 { isFreetextMatch } = require("./filter-text-utils");
+
/**
* Predicates used when filtering items.
*
* @param object item
* The filtered item.
* @return boolean
* True if the item should be visible, false otherwise.
*/
@@ -97,37 +99,24 @@ function isWS({ requestHeaders, response
return true;
}
function isOther(item) {
let tests = [isHtml, isCss, isJs, isXHR, isFont, isImage, isMedia, isFlash, isWS];
return tests.every(is => !is(item));
}
-function isFreetextMatch({ url }, text) {
- let lowerCaseUrl = url.toLowerCase();
- let lowerCaseText = text.toLowerCase();
- let textLength = text.length;
- // Support negative filtering
- if (text.startsWith("-") && textLength > 1) {
- lowerCaseText = lowerCaseText.substring(1, textLength);
- return !lowerCaseUrl.includes(lowerCaseText);
- }
-
- // no text is a positive match
- return !text || lowerCaseUrl.includes(lowerCaseText);
-}
-
-exports.Filters = {
- all: all,
- html: isHtml,
- css: isCss,
- js: isJs,
- xhr: isXHR,
- fonts: isFont,
- images: isImage,
- media: isMedia,
- flash: isFlash,
- ws: isWS,
- other: isOther,
+module.exports = {
+ Filters: {
+ all: all,
+ html: isHtml,
+ css: isCss,
+ js: isJs,
+ xhr: isXHR,
+ fonts: isFont,
+ images: isImage,
+ media: isMedia,
+ flash: isFlash,
+ ws: isWS,
+ other: isOther,
+ },
+ isFreetextMatch,
};
-
-exports.isFreetextMatch = isFreetextMatch;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/filter-text-utils.js
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2013 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+"use strict";
+
+const { HEADERS } = require("../constants");
+const HEADER_FILTERS = HEADERS
+ .filter(h => h.canFilter)
+ .map(h => h.filterKey || h.name);
+
+const FILTER_FLAGS = [
+ ...HEADER_FILTERS,
+ "scheme",
+ "mime-type",
+ "larger-than",
+ "is",
+];
+
+/*
+ The function `parseFilters` is from:
+ https://github.com/ChromeDevTools/devtools-frontend/
+
+ front_end/network/FilterSuggestionBuilder.js#L138-L163
+ Commit f340aefd7ec9b702de9366a812288cfb12111fce
+*/
+
+function parseFilters(query) {
+ let flags = [];
+ let text = [];
+ let parts = query.split(/\s+/);
+
+ for (let part of parts) {
+ if (!part) {
+ continue;
+ }
+ let colonIndex = part.indexOf(":");
+ if (colonIndex === -1) {
+ text.push(part);
+ continue;
+ }
+ let key = part.substring(0, colonIndex);
+ let negative = key.startsWith("-");
+ if (negative) {
+ key = key.substring(1);
+ }
+ if (!FILTER_FLAGS.includes(key)) {
+ text.push(part);
+ continue;
+ }
+ let value = part.substring(colonIndex + 1);
+ value = processFlagFilter(key, value);
+ flags.push({
+ type: key,
+ value,
+ negative,
+ });
+ }
+
+ return { text, flags };
+}
+
+function processFlagFilter(type, value) {
+ switch (type) {
+ case "size":
+ case "transferred":
+ case "larger-than":
+ let multiplier = 1;
+ if (value.endsWith("k")) {
+ multiplier = 1024;
+ value = value.substring(0, value.length - 1);
+ } else if (value.endsWith("m")) {
+ multiplier = 1024 * 1024;
+ value = value.substring(0, value.length - 1);
+ }
+ let quantity = Number(value);
+ if (isNaN(quantity)) {
+ return null;
+ }
+ return quantity * multiplier;
+ default:
+ return value.toLowerCase();
+ }
+}
+
+function getSizeOrder(size) {
+ return Math.round(Math.log10(size));
+}
+
+function isFlagFilterMatch(item, { type, value, negative }) {
+ let match = true;
+ switch (type) {
+ case "status-code":
+ match = item.status === value;
+ break;
+ case "method":
+ match = item.method.toLowerCase() === value;
+ break;
+ case "domain":
+ match = item.urlDetails.host.toLowerCase().includes(value);
+ break;
+ case "cause":
+ let causeType = item.cause.type;
+ match = typeof causeType === "string" ?
+ causeType.toLowerCase().includes(value) : false;
+ break;
+ case "transferred":
+ if (item.fromCache) {
+ match = false;
+ } else {
+ match = getSizeOrder(value) === getSizeOrder(item.transferredSize);
+ }
+ break;
+ case "size":
+ match = getSizeOrder(value) === getSizeOrder(item.contentSize);
+ break;
+ case "larger-than":
+ match = item.contentSize > value;
+ break;
+ case "mime-type":
+ match = item.mimeType.includes(value);
+ break;
+ case "is":
+ if (value === "from-cache" ||
+ value === "cached") {
+ match = item.fromCache || item.status === "304";
+ } else if (value === "running") {
+ match = !item.status;
+ }
+ break;
+ case "scheme":
+ let scheme = new URL(item.url).protocol.replace(":", "").toLowerCase();
+ match = scheme === value;
+ break;
+ }
+ if (negative) {
+ return !match;
+ }
+ return match;
+}
+
+function isTextFilterMatch({ url }, text) {
+ let lowerCaseUrl = url.toLowerCase();
+ let lowerCaseText = text.toLowerCase();
+ let textLength = text.length;
+ // Support negative filtering
+ if (text.startsWith("-") && textLength > 1) {
+ lowerCaseText = lowerCaseText.substring(1, textLength);
+ return !lowerCaseUrl.includes(lowerCaseText);
+ }
+
+ // no text is a positive match
+ return !text || lowerCaseUrl.includes(lowerCaseText);
+}
+
+function isFreetextMatch(item, text) {
+ if (!text) {
+ return true;
+ }
+
+ let filters = parseFilters(text);
+ let match = true;
+
+ for (let textFilter of filters.text) {
+ match = match && isTextFilterMatch(item, textFilter);
+ }
+
+ for (let flagFilter of filters.flags) {
+ match = match && isFlagFilterMatch(item, flagFilter);
+ }
+
+ return match;
+}
+
+module.exports = {
+ isFreetextMatch,
+};
--- a/devtools/client/netmonitor/src/utils/moz.build
+++ b/devtools/client/netmonitor/src/utils/moz.build
@@ -2,15 +2,16 @@
# 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/.
DevToolsModules(
'client.js',
'create-store.js',
'filter-predicates.js',
+ 'filter-text-utils.js',
'format-utils.js',
'l10n.js',
'mdn-utils.js',
'prefs.js',
'request-utils.js',
'sort-predicates.js',
)
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -103,16 +103,17 @@ skip-if = (os == 'linux' && bits == 32 &
[browser_net_frame.js]
[browser_net_header-docs.js]
skip-if = (os == 'linux' && debug && bits == 32) # Bug 1321434
[browser_net_filter-01.js]
skip-if = (os == 'linux' && debug && bits == 32) # Bug 1303439
[browser_net_filter-02.js]
[browser_net_filter-03.js]
[browser_net_filter-04.js]
+[browser_net_filter-flags.js]
[browser_net_footer-summary.js]
[browser_net_icon-preview.js]
[browser_net_image-tooltip.js]
[browser_net_json-b64.js]
[browser_net_json-null.js]
[browser_net_json-long.js]
[browser_net_json-malformed.js]
[browser_net_json_custom_mime.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_filter-flags.js
@@ -0,0 +1,217 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test different text filtering flags
+ */
+const REQUESTS = [
+ { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=Sample" },
+ { url: "sjs_content-type-test-server.sjs?fmt=css&text=sample" },
+ { url: "sjs_content-type-test-server.sjs?fmt=js&text=sample" },
+ { url: "sjs_content-type-test-server.sjs?fmt=font" },
+ { url: "sjs_content-type-test-server.sjs?fmt=image" },
+ { url: "sjs_content-type-test-server.sjs?fmt=audio" },
+ { url: "sjs_content-type-test-server.sjs?fmt=video" },
+ { url: "sjs_status-codes-test-server.sjs?sts=304" },
+];
+
+const EXPECTED_REQUESTS = [
+ {
+ method: "GET",
+ url: CONTENT_TYPE_SJS + "?fmt=html",
+ data: {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ url: CONTENT_TYPE_SJS + "?fmt=css",
+ data: {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "css",
+ fullMimeType: "text/css; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ url: CONTENT_TYPE_SJS + "?fmt=js",
+ data: {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "js",
+ fullMimeType: "application/javascript; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ url: CONTENT_TYPE_SJS + "?fmt=font",
+ data: {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "woff",
+ fullMimeType: "font/woff"
+ }
+ },
+ {
+ method: "GET",
+ url: CONTENT_TYPE_SJS + "?fmt=image",
+ data: {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "png",
+ fullMimeType: "image/png"
+ }
+ },
+ {
+ method: "GET",
+ url: CONTENT_TYPE_SJS + "?fmt=audio",
+ data: {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "ogg",
+ fullMimeType: "audio/ogg"
+ }
+ },
+ {
+ method: "GET",
+ url: CONTENT_TYPE_SJS + "?fmt=video",
+ data: {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "webm",
+ fullMimeType: "video/webm"
+ },
+ },
+ {
+ method: "GET",
+ url: STATUS_CODES_SJS + "?sts=304",
+ data: {
+ status: 304,
+ statusText: "Not Modified",
+ displayedStatus: "304",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8"
+ }
+ },
+];
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(FILTERING_URL);
+ let { document, gStore, windowRequire } = monitor.panelWin;
+ let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+ let {
+ getDisplayedRequests,
+ getSortedRequests,
+ } = windowRequire("devtools/client/netmonitor/src/selectors/index");
+
+ gStore.dispatch(Actions.batchEnable(false));
+
+ function setFreetextFilter(value) {
+ gStore.dispatch(Actions.setRequestFilterText(value));
+ }
+
+ info("Starting test... ");
+
+ let waitNetwork = waitForNetworkEvents(monitor, 8);
+ loadCommonFrameScript();
+ yield performRequestsInContent(REQUESTS);
+ yield waitNetwork;
+
+ // Test running flag once requests finish running
+ setFreetextFilter("is:running");
+ testContents([0, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Test cached flag
+ setFreetextFilter("is:from-cache");
+ testContents([0, 0, 0, 0, 0, 0, 0, 1]);
+
+ setFreetextFilter("is:cached");
+ testContents([0, 0, 0, 0, 0, 0, 0, 1]);
+
+ // Test negative cached flag
+ setFreetextFilter("-is:from-cache");
+ testContents([1, 1, 1, 1, 1, 1, 1, 0]);
+
+ setFreetextFilter("-is:cached");
+ testContents([1, 1, 1, 1, 1, 1, 1, 0]);
+
+ // Test status-code flag
+ setFreetextFilter("status-code:200");
+ testContents([1, 1, 1, 1, 1, 1, 1, 0]);
+
+ // Test status-code negative flag
+ setFreetextFilter("-status-code:200");
+ testContents([0, 0, 0, 0, 0, 0, 0, 1]);
+
+ // Test mime-type flag
+ setFreetextFilter("mime-type:HtmL");
+ testContents([1, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Test mime-type negative flag
+ setFreetextFilter("-mime-type:HtmL");
+ testContents([0, 1, 1, 1, 1, 1, 1, 1]);
+
+ // Test method flag
+ setFreetextFilter("method:get");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1]);
+
+ // Test unmatched method flag
+ setFreetextFilter("method:post");
+ testContents([0, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Test scheme flag (all requests are http)
+ setFreetextFilter("scheme:http");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1]);
+
+ setFreetextFilter("scheme:https");
+ testContents([0, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Test mixing flags
+ setFreetextFilter("-mime-type:HtmL status-code:200");
+ testContents([0, 1, 1, 1, 1, 1, 1, 0]);
+
+ yield teardown(monitor);
+
+ function testContents(visibility) {
+ const items = getSortedRequests(gStore.getState());
+ const visibleItems = getDisplayedRequests(gStore.getState());
+
+ is(items.size, visibility.length,
+ "There should be a specific amount of items in the requests menu.");
+ is(visibleItems.size, visibility.filter(e => e).length,
+ "There should be a specific amount of visible items in the requests menu.");
+
+ for (let i = 0; i < visibility.length; i++) {
+ let itemId = items.get(i).id;
+ let shouldBeVisible = !!visibility[i];
+ let isThere = visibleItems.some(r => r.id == itemId);
+ is(isThere, shouldBeVisible,
+ `The item at index ${i} has visibility=${shouldBeVisible}`);
+
+ if (shouldBeVisible) {
+ let { method, url, data } = EXPECTED_REQUESTS[i];
+ verifyRequestItemTarget(
+ document,
+ getDisplayedRequests(gStore.getState()),
+ getSortedRequests(gStore.getState()).get(i),
+ method,
+ url,
+ data
+ );
+ }
+ }
+ }
+});
--- a/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs
+++ b/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs
@@ -19,16 +19,19 @@ function handleRequest(request, response
response.setStatusLine(request.httpVersion, 101, "Switching Protocols");
break;
case "200":
response.setStatusLine(request.httpVersion, 202, "Created");
break;
case "300":
response.setStatusLine(request.httpVersion, 303, "See Other");
break;
+ case "304":
+ response.setStatusLine(request.httpVersion, 304, "Not Modified");
+ break;
case "400":
response.setStatusLine(request.httpVersion, 404, "Not Found");
break;
case "500":
response.setStatusLine(request.httpVersion, 501, "Not Implemented");
break;
case "ok":
response.setStatusLine(request.httpVersion, 200, "OK");
--- a/toolkit/content/license.html
+++ b/toolkit/content/license.html
@@ -3252,18 +3252,19 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE PO
<hr>
<h1><a id="google-bsd"></a>Google BSD License</h1>
<p>This license applies to files in the directories
- <span class="path">toolkit/crashreporter/google-breakpad/</span> and
- <span class="path">toolkit/components/protobuf/</span>.</p>
+ <span class="path">toolkit/crashreporter/google-breakpad/</span>,
+ <span class="path">toolkit/components/protobuf/</span> and
+ <span class="path">devtools/client/netmonitor/src/utils/filter-text-utils.js.</p>
<pre>
Copyright (c) 2006, Google Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met: