From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../src/widgets/HeadersPanelContextMenu.js | 137 ++++ .../src/widgets/PropertiesViewContextMenu.js | 113 +++ .../src/widgets/RequestBlockingContextMenu.js | 78 ++ .../src/widgets/RequestListContextMenu.js | 797 +++++++++++++++++++++ .../src/widgets/RequestListHeaderContextMenu.js | 105 +++ .../netmonitor/src/widgets/WaterfallBackground.js | 163 +++++ devtools/client/netmonitor/src/widgets/moz.build | 12 + 7 files changed, 1405 insertions(+) create mode 100644 devtools/client/netmonitor/src/widgets/HeadersPanelContextMenu.js create mode 100644 devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js create mode 100644 devtools/client/netmonitor/src/widgets/RequestBlockingContextMenu.js create mode 100644 devtools/client/netmonitor/src/widgets/RequestListContextMenu.js create mode 100644 devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js create mode 100644 devtools/client/netmonitor/src/widgets/WaterfallBackground.js create mode 100644 devtools/client/netmonitor/src/widgets/moz.build (limited to 'devtools/client/netmonitor/src/widgets') diff --git a/devtools/client/netmonitor/src/widgets/HeadersPanelContextMenu.js b/devtools/client/netmonitor/src/widgets/HeadersPanelContextMenu.js new file mode 100644 index 0000000000..6f159ded73 --- /dev/null +++ b/devtools/client/netmonitor/src/widgets/HeadersPanelContextMenu.js @@ -0,0 +1,137 @@ +/* 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 { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const { + contextMenuFormatters, +} = require("resource://devtools/client/netmonitor/src/utils/context-menu-utils.js"); + +loader.lazyRequireGetter( + this, + "copyString", + "resource://devtools/shared/platform/clipboard.js", + true +); +loader.lazyRequireGetter( + this, + "showMenu", + "resource://devtools/client/shared/components/menu/utils.js", + true +); + +class HeadersPanelContextMenu { + constructor(props = {}) { + this.props = props; + this.copyAll = this.copyAll.bind(this); + this.copyValue = this.copyValue.bind(this); + } + + /** + * Handle the context menu opening. + * @param {Object} event open event + * @param {Object} selection object representing the current selection + */ + open(event = {}, selection) { + const { target } = event; + const menuItems = [ + { + id: "headers-panel-context-menu-copyvalue", + label: L10N.getStr("netmonitor.context.copyValue"), + accesskey: L10N.getStr("netmonitor.context.copyValue.accesskey"), + click: () => { + const { name, value } = getSummaryContent( + target.closest(".tabpanel-summary-container") + ); + this.copyValue( + { name, value, object: null, hasChildren: false }, + selection + ); + }, + }, + { + id: "headers-panel-context-menu-copyall", + label: L10N.getStr("netmonitor.context.copyAll"), + accesskey: L10N.getStr("netmonitor.context.copyAll.accesskey"), + click: () => { + const root = target.closest(".summary"); + const object = {}; + if (root) { + const { children } = root; + for (let i = 0; i < children.length; i++) { + const content = getSummaryContent(children[i]); + object[content.name] = content.value; + } + } + return this.copyAll(object, selection); + }, + }, + ]; + + showMenu(menuItems, { + screenX: event.screenX, + screenY: event.screenY, + }); + } + + /** + * Copies all. + * @param {Object} object the whole tree data + * @param {Object} selection object representing the current selection + */ + copyAll(object, selection) { + let buffer = ""; + if (selection.toString() !== "") { + buffer = selection.toString(); + } else { + const { customFormatters } = this.props; + buffer = contextMenuFormatters.baseCopyAllFormatter(object); + if (customFormatters?.copyAllFormatter) { + buffer = customFormatters.copyAllFormatter( + object, + contextMenuFormatters.baseCopyAllFormatter + ); + } + } + try { + copyString(buffer); + } catch (error) {} + } + + /** + * Copies the value of a single item. + * @param {Object} object data object for specific node + * @param {Object} selection object representing the current selection + */ + copyValue(object, selection) { + let buffer = ""; + if (selection.toString() !== "") { + buffer = selection.toString(); + } else { + const { customFormatters } = this.props; + buffer = contextMenuFormatters.baseCopyFormatter(object); + if (customFormatters?.copyFormatter) { + buffer = customFormatters.copyFormatter( + object, + contextMenuFormatters.baseCopyFormatter + ); + } + } + try { + copyString(buffer); + } catch (error) {} + } +} + +function getSummaryContent(el) { + return { + name: el.querySelector(".tabpanel-summary-label").textContent, + value: el.querySelector(".tabpanel-summary-value").textContent, + }; +} + +module.exports = HeadersPanelContextMenu; diff --git a/devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js b/devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js new file mode 100644 index 0000000000..8d823444c2 --- /dev/null +++ b/devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js @@ -0,0 +1,113 @@ +/* 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 { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const { + contextMenuFormatters, +} = require("resource://devtools/client/netmonitor/src/utils/context-menu-utils.js"); + +loader.lazyRequireGetter( + this, + "copyString", + "resource://devtools/shared/platform/clipboard.js", + true +); +loader.lazyRequireGetter( + this, + "showMenu", + "resource://devtools/client/shared/components/menu/utils.js", + true +); + +class PropertiesViewContextMenu { + constructor(props = {}) { + this.props = props; + this.copyAll = this.copyAll.bind(this); + this.copyValue = this.copyValue.bind(this); + } + + /** + * Handle the context menu opening. + * @param {Object} event open event + * @param {Object} selection object representing the current selection + * @param {Object} data object containing information + * @param {Object} data.member member of the right-clicked row + * @param {Object} data.object the whole tree data + */ + open(event = {}, selection, { member, object }) { + const menuItems = [ + { + id: "properties-view-context-menu-copyvalue", + label: L10N.getStr("netmonitor.context.copyValue"), + accesskey: L10N.getStr("netmonitor.context.copyValue.accesskey"), + click: () => this.copyValue(member, selection), + }, + { + id: "properties-view-context-menu-copyall", + label: L10N.getStr("netmonitor.context.copyAll"), + accesskey: L10N.getStr("netmonitor.context.copyAll.accesskey"), + click: () => this.copyAll(object, selection), + }, + ]; + + showMenu(menuItems, { + screenX: event.screenX, + screenY: event.screenY, + }); + } + + /** + * Copies all. + * @param {Object} object the whole tree data + * @param {Object} selection object representing the current selection + */ + copyAll(object, selection) { + let buffer = ""; + if (selection.toString() !== "") { + buffer = selection.toString(); + } else { + const { customFormatters } = this.props; + buffer = contextMenuFormatters.baseCopyAllFormatter(object); + if (customFormatters?.copyAllFormatter) { + buffer = customFormatters.copyAllFormatter( + object, + contextMenuFormatters.baseCopyAllFormatter + ); + } + } + try { + copyString(buffer); + } catch (error) {} + } + + /** + * Copies the value of a single item. + * @param {Object} member member of the right-clicked row + * @param {Object} selection object representing the current selection + */ + copyValue(member, selection) { + let buffer = ""; + if (selection.toString() !== "") { + buffer = selection.toString(); + } else { + const { customFormatters } = this.props; + buffer = contextMenuFormatters.baseCopyFormatter(member); + if (customFormatters?.copyFormatter) { + buffer = customFormatters.copyFormatter( + member, + contextMenuFormatters.baseCopyFormatter + ); + } + } + try { + copyString(buffer); + } catch (error) {} + } +} + +module.exports = PropertiesViewContextMenu; diff --git a/devtools/client/netmonitor/src/widgets/RequestBlockingContextMenu.js b/devtools/client/netmonitor/src/widgets/RequestBlockingContextMenu.js new file mode 100644 index 0000000000..f6a10b0ca3 --- /dev/null +++ b/devtools/client/netmonitor/src/widgets/RequestBlockingContextMenu.js @@ -0,0 +1,78 @@ +/* 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 { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + +loader.lazyRequireGetter( + this, + "showMenu", + "resource://devtools/client/shared/components/menu/utils.js", + true +); + +class RequestBlockingContextMenu { + constructor(props) { + this.props = props; + } + + createMenu(contextMenuOptions) { + const { + removeAllBlockedUrls, + disableAllBlockedUrls, + enableAllBlockedUrls, + } = this.props; + + const { disableDisableAllBlockedUrls, disableEnableAllBlockedUrls } = + contextMenuOptions; + + const menu = [ + { + id: "request-blocking-enable-all", + label: L10N.getStr( + "netmonitor.requestBlockingMenu.enableAllBlockedUrls" + ), + accesskey: "", + disabled: disableEnableAllBlockedUrls, + visible: true, + click: () => enableAllBlockedUrls(), + }, + { + id: "request-blocking-disable-all", + label: L10N.getStr( + "netmonitor.requestBlockingMenu.disableAllBlockedUrls" + ), + accesskey: "", + disabled: disableDisableAllBlockedUrls, + visible: true, + click: () => disableAllBlockedUrls(), + }, + { + id: "request-blocking-remove-all", + label: L10N.getStr( + "netmonitor.requestBlockingMenu.removeAllBlockedUrls" + ), + accesskey: "", + visible: true, + click: () => removeAllBlockedUrls(), + }, + ]; + + return menu; + } + + open(event, contextMenuOptions) { + const menu = this.createMenu(contextMenuOptions); + + showMenu(menu, { + screenX: event.screenX, + screenY: event.screenY, + }); + } +} + +module.exports = RequestBlockingContextMenu; diff --git a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js new file mode 100644 index 0000000000..7c19e912c6 --- /dev/null +++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js @@ -0,0 +1,797 @@ +/* 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 { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const { + formDataURI, + getUrlQuery, + getUrlBaseName, + parseQueryString, + getRequestHeadersRawText, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const { + hasMatchingBlockingRequestPattern, +} = require("resource://devtools/client/netmonitor/src/utils/request-blocking.js"); + +loader.lazyRequireGetter( + this, + "Curl", + "resource://devtools/client/shared/curl.js", + true +); +loader.lazyRequireGetter( + this, + "saveAs", + "resource://devtools/shared/DevToolsUtils.js", + true +); +loader.lazyRequireGetter( + this, + "PowerShell", + "resource://devtools/client/netmonitor/src/utils/powershell.js", + true +); +loader.lazyRequireGetter( + this, + "copyString", + "resource://devtools/shared/platform/clipboard.js", + true +); +loader.lazyRequireGetter( + this, + "showMenu", + "resource://devtools/client/shared/components/menu/utils.js", + true +); +loader.lazyRequireGetter( + this, + "HarMenuUtils", + "resource://devtools/client/netmonitor/src/har/har-menu-utils.js", + true +); + +const { OS } = Services.appinfo; + +class RequestListContextMenu { + constructor(props) { + this.props = props; + } + + createCopySubMenu(clickedRequest, requests) { + const { connector } = this.props; + + const { + id, + formDataSections, + method, + mimeType, + httpVersion, + requestHeaders, + requestHeadersAvailable, + requestPostData, + requestPostDataAvailable, + responseHeaders, + responseHeadersAvailable, + responseContent, + responseContentAvailable, + url, + } = clickedRequest; + + const copySubMenu = []; + + copySubMenu.push({ + id: "request-list-context-copy-url", + label: L10N.getStr("netmonitor.context.copyUrl"), + accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"), + visible: !!clickedRequest, + click: () => this.copyUrl(url), + }); + + copySubMenu.push({ + id: "request-list-context-copy-url-params", + label: L10N.getStr("netmonitor.context.copyUrlParams"), + accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"), + visible: !!(clickedRequest && getUrlQuery(url)), + click: () => this.copyUrlParams(url), + }); + + copySubMenu.push({ + id: "request-list-context-copy-post-data", + label: L10N.getFormatStr("netmonitor.context.copyRequestData", method), + accesskey: L10N.getStr("netmonitor.context.copyRequestData.accesskey"), + // Menu item will be visible even if data hasn't arrived, so we need to check + // *Available property and then fetch data lazily once user triggers the action. + visible: !!( + clickedRequest && + (requestPostDataAvailable || requestPostData) + ), + click: () => this.copyPostData(id, formDataSections, requestPostData), + }); + + if (OS === "WINNT") { + copySubMenu.push({ + id: "request-list-context-copy-as-curl-win", + label: L10N.getFormatStr( + "netmonitor.context.copyAsCurl.win", + L10N.getStr("netmonitor.context.copyAsCurl") + ), + accesskey: L10N.getStr("netmonitor.context.copyAsCurl.win.accesskey"), + // Menu item will be visible even if data hasn't arrived, so we need to check + // *Available property and then fetch data lazily once user triggers the action. + visible: !!clickedRequest, + click: () => + this.copyAsCurl( + id, + url, + method, + httpVersion, + requestHeaders, + requestPostData, + responseHeaders, + "WINNT" + ), + }); + + copySubMenu.push({ + id: "request-list-context-copy-as-curl-posix", + label: L10N.getFormatStr( + "netmonitor.context.copyAsCurl.posix", + L10N.getStr("netmonitor.context.copyAsCurl") + ), + accesskey: L10N.getStr("netmonitor.context.copyAsCurl.posix.accesskey"), + // Menu item will be visible even if data hasn't arrived, so we need to check + // *Available property and then fetch data lazily once user triggers the action. + visible: !!clickedRequest, + click: () => + this.copyAsCurl( + id, + url, + method, + httpVersion, + requestHeaders, + requestPostData, + responseHeaders, + "Linux" + ), + }); + } else { + copySubMenu.push({ + id: "request-list-context-copy-as-curl", + label: L10N.getStr("netmonitor.context.copyAsCurl"), + accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"), + // Menu item will be visible even if data hasn't arrived, so we need to check + // *Available property and then fetch data lazily once user triggers the action. + visible: !!clickedRequest, + click: () => + this.copyAsCurl( + id, + url, + method, + httpVersion, + requestHeaders, + requestPostData, + responseHeaders + ), + }); + } + + copySubMenu.push({ + id: "request-list-context-copy-as-powershell", + label: L10N.getStr("netmonitor.context.copyAsPowerShell"), + accesskey: L10N.getStr("netmonitor.context.copyAsPowerShell.accesskey"), + // Menu item will be visible even if data hasn't arrived, so we need to check + // *Available property and then fetch data lazily once user triggers the action. + visible: !!clickedRequest, + click: () => this.copyAsPowerShell(clickedRequest), + }); + + copySubMenu.push({ + id: "request-list-context-copy-as-fetch", + label: L10N.getStr("netmonitor.context.copyAsFetch"), + accesskey: L10N.getStr("netmonitor.context.copyAsFetch.accesskey"), + visible: !!clickedRequest, + click: () => + this.copyAsFetch(id, url, method, requestHeaders, requestPostData), + }); + + copySubMenu.push({ + type: "separator", + visible: copySubMenu.slice(0, 4).some(subMenu => subMenu.visible), + }); + + copySubMenu.push({ + id: "request-list-context-copy-request-headers", + label: L10N.getStr("netmonitor.context.copyRequestHeaders"), + accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"), + // Menu item will be visible even if data hasn't arrived, so we need to check + // *Available property and then fetch data lazily once user triggers the action. + visible: !!( + clickedRequest && + (requestHeadersAvailable || requestHeaders) + ), + click: () => this.copyRequestHeaders(id, clickedRequest), + }); + + copySubMenu.push({ + id: "response-list-context-copy-response-headers", + label: L10N.getStr("netmonitor.context.copyResponseHeaders"), + accesskey: L10N.getStr( + "netmonitor.context.copyResponseHeaders.accesskey" + ), + // Menu item will be visible even if data hasn't arrived, so we need to check + // *Available property and then fetch data lazily once user triggers the action. + visible: !!( + clickedRequest && + (responseHeadersAvailable || responseHeaders) + ), + click: () => this.copyResponseHeaders(id, responseHeaders), + }); + + copySubMenu.push({ + id: "request-list-context-copy-response", + label: L10N.getStr("netmonitor.context.copyResponse"), + accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"), + // Menu item will be visible even if data hasn't arrived, so we need to check + // *Available property and then fetch data lazily once user triggers the action. + visible: !!( + clickedRequest && + (responseContentAvailable || responseContent) + ), + click: () => this.copyResponse(id, responseContent), + }); + + copySubMenu.push({ + id: "request-list-context-copy-image-as-data-uri", + label: L10N.getStr("netmonitor.context.copyImageAsDataUri"), + accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"), + visible: !!( + clickedRequest && + (responseContentAvailable || responseContent) && + mimeType && + mimeType.includes("image/") + ), + click: () => this.copyImageAsDataUri(id, mimeType, responseContent), + }); + + copySubMenu.push({ + type: "separator", + visible: copySubMenu.slice(5, 9).some(subMenu => subMenu.visible), + }); + + copySubMenu.push({ + id: "request-list-context-copy-all-as-har", + label: L10N.getStr("netmonitor.context.copyAllAsHar"), + accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"), + visible: !!requests.length, + click: () => HarMenuUtils.copyAllAsHar(requests, connector), + }); + + return copySubMenu; + } + + createMenu(clickedRequest, requests, blockedUrls) { + const { + connector, + cloneRequest, + openDetailsPanelTab, + openHTTPCustomRequestTab, + closeHTTPCustomRequestTab, + sendCustomRequest, + sendHTTPCustomRequest, + openStatistics, + openRequestInTab, + openRequestBlockingAndAddUrl, + openRequestBlockingAndDisableUrls, + removeBlockedUrl, + } = this.props; + + const { + id, + isCustom, + method, + cause, + isEventStream, + mimeType, + requestHeaders, + requestPostData, + responseContent, + responseContentAvailable, + url, + } = clickedRequest; + + const copySubMenu = this.createCopySubMenu(clickedRequest, requests); + const newEditAndResendPref = Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend" + ); + + return [ + { + label: L10N.getStr("netmonitor.context.copyValue"), + accesskey: L10N.getStr("netmonitor.context.copyValue.accesskey"), + visible: !!clickedRequest, + submenu: copySubMenu, + }, + { + id: "request-list-context-save-all-as-har", + label: L10N.getStr("netmonitor.context.saveAllAsHar"), + accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"), + visible: !!requests.length, + click: () => HarMenuUtils.saveAllAsHar(requests, connector), + }, + { + id: "request-list-context-save-response-as", + label: L10N.getStr("netmonitor.context.saveResponseAs"), + accesskey: L10N.getStr("netmonitor.context.saveResponseAs.accesskey"), + visible: !!( + clickedRequest && + (responseContentAvailable || responseContent) && + mimeType && + // Websockets and server-sent events don't have a real 'response' for us to save + cause.type !== "websocket" && + !isEventStream + ), + click: () => this.saveResponseAs(id, url, responseContent), + }, + { + type: "separator", + visible: copySubMenu.slice(10, 14).some(subMenu => subMenu.visible), + }, + { + id: "request-list-context-resend-only", + label: L10N.getStr("netmonitor.context.resend.label"), + accesskey: L10N.getStr("netmonitor.context.resend.accesskey"), + visible: !!(clickedRequest && !isCustom), + click: () => { + if (!newEditAndResendPref) { + cloneRequest(id); + sendCustomRequest(); + } else { + sendHTTPCustomRequest(clickedRequest); + } + }, + }, + + { + id: "request-list-context-edit-resend", + label: L10N.getStr("netmonitor.context.editAndResend"), + accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"), + visible: !!(clickedRequest && !isCustom), + click: () => { + this.fetchRequestHeaders(id).then(() => { + if (!newEditAndResendPref) { + cloneRequest(id); + openDetailsPanelTab(); + } else { + closeHTTPCustomRequestTab(); + openHTTPCustomRequestTab(); + } + }); + }, + }, + { + id: "request-list-context-block-url", + label: L10N.getStr("netmonitor.context.blockURL"), + visible: !hasMatchingBlockingRequestPattern( + blockedUrls, + clickedRequest.url + ), + click: () => { + openRequestBlockingAndAddUrl(clickedRequest.url); + }, + }, + { + id: "request-list-context-unblock-url", + label: L10N.getStr("netmonitor.context.unblockURL"), + visible: hasMatchingBlockingRequestPattern( + blockedUrls, + clickedRequest.url + ), + click: () => { + if ( + blockedUrls.find(blockedUrl => blockedUrl === clickedRequest.url) + ) { + removeBlockedUrl(clickedRequest.url); + } else { + openRequestBlockingAndDisableUrls(clickedRequest.url); + } + }, + }, + { + type: "separator", + visible: copySubMenu.slice(15, 16).some(subMenu => subMenu.visible), + }, + { + id: "request-list-context-newtab", + label: L10N.getStr("netmonitor.context.newTab"), + accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"), + visible: !!clickedRequest, + click: () => openRequestInTab(id, url, requestHeaders, requestPostData), + }, + { + id: "request-list-context-open-in-debugger", + label: L10N.getStr("netmonitor.context.openInDebugger"), + accesskey: L10N.getStr("netmonitor.context.openInDebugger.accesskey"), + visible: !!( + clickedRequest && + mimeType && + mimeType.includes("javascript") + ), + click: () => this.openInDebugger(url), + }, + { + id: "request-list-context-open-in-style-editor", + label: L10N.getStr("netmonitor.context.openInStyleEditor"), + accesskey: L10N.getStr( + "netmonitor.context.openInStyleEditor.accesskey" + ), + visible: !!( + clickedRequest && + Services.prefs.getBoolPref("devtools.styleeditor.enabled") && + mimeType && + mimeType.includes("css") + ), + click: () => this.openInStyleEditor(url), + }, + { + id: "request-list-context-perf", + label: L10N.getStr("netmonitor.context.perfTools"), + accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"), + visible: !!requests.length, + click: () => openStatistics(true), + }, + { + type: "separator", + }, + { + id: "request-list-context-use-as-fetch", + label: L10N.getStr("netmonitor.context.useAsFetch"), + accesskey: L10N.getStr("netmonitor.context.useAsFetch.accesskey"), + visible: !!clickedRequest, + click: () => + this.useAsFetch(id, url, method, requestHeaders, requestPostData), + }, + ]; + } + + open(event, clickedRequest, requests, blockedUrls) { + const menu = this.createMenu(clickedRequest, requests, blockedUrls); + + showMenu(menu, { + screenX: event.screenX, + screenY: event.screenY, + }); + } + + /** + * Opens selected item in the debugger + */ + openInDebugger(url) { + const toolbox = this.props.connector.getToolbox(); + toolbox.viewGeneratedSourceInDebugger(url); + } + + /** + * Opens selected item in the style editor + */ + openInStyleEditor(url) { + const toolbox = this.props.connector.getToolbox(); + toolbox.viewGeneratedSourceInStyleEditor(url); + } + + /** + * Copy the request url from the currently selected item. + */ + copyUrl(url) { + copyString(url); + } + + /** + * Copy the request url query string parameters from the currently + * selected item. + */ + copyUrlParams(url) { + const params = getUrlQuery(url).split("&"); + copyString(params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n")); + } + + /** + * Copy the request form data parameters (or raw payload) from + * the currently selected item. + */ + async copyPostData(id, formDataSections, requestPostData) { + let params = []; + // Try to extract any form data parameters if formDataSections is already + // available, which is only true if RequestPanel has ever been mounted before. + if (formDataSections) { + formDataSections.forEach(section => { + const paramsArray = parseQueryString(section); + if (paramsArray) { + params = [...params, ...paramsArray]; + } + }); + } + + let string = params + .map(param => param.name + (param.value ? "=" + param.value : "")) + .join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n"); + + // Fall back to raw payload. + if (!string) { + requestPostData = + requestPostData || + (await this.props.connector.requestData(id, "requestPostData")); + + string = requestPostData.postData.text; + if (Services.appinfo.OS !== "WINNT") { + string = string.replace(/\r/g, ""); + } + } + copyString(string); + } + + /** + * Copy a cURL command from the currently selected item. + */ + async copyAsCurl( + id, + url, + method, + httpVersion, + requestHeaders, + requestPostData, + responseHeaders, + platform + ) { + requestHeaders = + requestHeaders || + (await this.props.connector.requestData(id, "requestHeaders")); + + requestPostData = + requestPostData || + (await this.props.connector.requestData(id, "requestPostData")); + + responseHeaders = + responseHeaders || + (await this.props.connector.requestData(id, "responseHeaders")); + + // Create a sanitized object for the Curl command generator. + const data = { + url, + method, + headers: requestHeaders.headers, + responseHeaders: responseHeaders.headers, + httpVersion, + postDataText: requestPostData ? requestPostData.postData.text : "", + }; + copyString(Curl.generateCommand(data, platform)); + } + + async copyAsPowerShell(request) { + let { id, url, method, requestHeaders, requestPostData, requestCookies } = + request; + + requestHeaders = + requestHeaders || + (await this.props.connector.requestData(id, "requestHeaders")); + + requestPostData = + requestPostData || + (await this.props.connector.requestData(id, "requestPostData")); + + requestCookies = + requestCookies || + (await this.props.connector.requestData(id, "requestCookies")); + + return copyString( + PowerShell.generateCommand( + url, + method, + requestHeaders.headers, + requestPostData.postData, + requestCookies.cookies || requestCookies + ) + ); + } + + /** + * Generate fetch string + */ + async generateFetchString(id, url, method, requestHeaders, requestPostData) { + requestHeaders = + requestHeaders || + (await this.props.connector.requestData(id, "requestHeaders")); + + requestPostData = + requestPostData || + (await this.props.connector.requestData(id, "requestPostData")); + + // https://fetch.spec.whatwg.org/#forbidden-header-name + const forbiddenHeaders = { + "accept-charset": 1, + "accept-encoding": 1, + "access-control-request-headers": 1, + "access-control-request-method": 1, + connection: 1, + "content-length": 1, + cookie: 1, + cookie2: 1, + date: 1, + dnt: 1, + expect: 1, + host: 1, + "keep-alive": 1, + origin: 1, + referer: 1, + te: 1, + trailer: 1, + "transfer-encoding": 1, + upgrade: 1, + via: 1, + }; + const credentialHeaders = { cookie: 1, authorization: 1 }; + + const headers = {}; + for (const { name, value } of requestHeaders.headers) { + if (!forbiddenHeaders[name.toLowerCase()]) { + headers[name] = value; + } + } + + const referrerHeader = requestHeaders.headers.find( + ({ name }) => name.toLowerCase() === "referer" + ); + + const referrerPolicy = requestHeaders.headers.find( + ({ name }) => name.toLowerCase() === "referrer-policy" + ); + + const referrer = referrerHeader ? referrerHeader.value : undefined; + const credentials = requestHeaders.headers.some( + ({ name }) => credentialHeaders[name.toLowerCase()] + ) + ? "include" + : "omit"; + + const fetchOptions = { + credentials, + headers, + referrer, + referrerPolicy, + body: requestPostData.postData.text, + method, + mode: "cors", + }; + + const options = JSON.stringify(fetchOptions, null, 4); + const fetchString = `await fetch("${url}", ${options});`; + return fetchString; + } + + /** + * Copy the currently selected item as fetch request. + */ + async copyAsFetch(id, url, method, requestHeaders, requestPostData) { + const fetchString = await this.generateFetchString( + id, + url, + method, + requestHeaders, + requestPostData + ); + copyString(fetchString); + } + + /** + * Open split console and fill it with fetch command for selected item + */ + async useAsFetch(id, url, method, requestHeaders, requestPostData) { + const fetchString = await this.generateFetchString( + id, + url, + method, + requestHeaders, + requestPostData + ); + const toolbox = this.props.connector.getToolbox(); + await toolbox.openSplitConsole(); + const { hud } = await toolbox.getPanel("webconsole"); + hud.setInputValue(fetchString); + } + + /** + * Copy the raw request headers from the currently selected item. + */ + async copyRequestHeaders( + id, + { method, httpVersion, requestHeaders, urlDetails } + ) { + requestHeaders = + requestHeaders || + (await this.props.connector.requestData(id, "requestHeaders")); + + let rawHeaders = getRequestHeadersRawText( + method, + httpVersion, + requestHeaders, + urlDetails + ); + + if (Services.appinfo.OS !== "WINNT") { + rawHeaders = rawHeaders.replace(/\r/g, ""); + } + copyString(rawHeaders); + } + + /** + * Copy the raw response headers from the currently selected item. + */ + async copyResponseHeaders(id, responseHeaders) { + responseHeaders = + responseHeaders || + (await this.props.connector.requestData(id, "responseHeaders")); + + let rawHeaders = responseHeaders.rawHeaders.trim(); + + if (Services.appinfo.OS !== "WINNT") { + rawHeaders = rawHeaders.replace(/\r/g, ""); + } + copyString(rawHeaders); + } + + /** + * Copy image as data uri. + */ + async copyImageAsDataUri(id, mimeType, responseContent) { + responseContent = + responseContent || + (await this.props.connector.requestData(id, "responseContent")); + + const { encoding, text } = responseContent.content; + copyString(formDataURI(mimeType, encoding, text)); + } + + /** + * Save response as. + */ + async saveResponseAs(id, url, responseContent) { + responseContent = + responseContent || + (await this.props.connector.requestData(id, "responseContent")); + + const { encoding, text } = responseContent.content; + const fileName = getUrlBaseName(url); + let data; + if (encoding === "base64") { + const decoded = atob(text); + data = new Uint8Array(decoded.length); + for (let i = 0; i < decoded.length; ++i) { + data[i] = decoded.charCodeAt(i); + } + } else { + data = new TextEncoder().encode(text); + } + saveAs(window, data, fileName); + } + + /** + * Copy response data as a string. + */ + async copyResponse(id, responseContent) { + responseContent = + responseContent || + (await this.props.connector.requestData(id, "responseContent")); + + copyString(responseContent.content.text); + } + + async fetchRequestHeaders(id) { + await this.props.connector.requestData(id, "requestHeaders"); + } +} + +module.exports = RequestListContextMenu; diff --git a/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js b/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js new file mode 100644 index 0000000000..02ee502790 --- /dev/null +++ b/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + showMenu, +} = require("resource://devtools/client/shared/components/menu/utils.js"); +const { + HEADERS, +} = require("resource://devtools/client/netmonitor/src/constants.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const { + getVisibleColumns, +} = require("resource://devtools/client/netmonitor/src/selectors/index.js"); + +const stringMap = HEADERS.filter(header => + header.hasOwnProperty("label") +).reduce((acc, { name, label }) => Object.assign(acc, { [name]: label }), {}); + +const subMenuMap = HEADERS.filter(header => + header.hasOwnProperty("subMenu") +).reduce( + (acc, { name, subMenu }) => Object.assign(acc, { [name]: subMenu }), + {} +); + +const nonLocalizedHeaders = HEADERS.filter(header => + header.hasOwnProperty("noLocalization") +).map(header => header.name); + +class RequestListHeaderContextMenu { + constructor(props) { + this.props = props; + } + + /** + * Handle the context menu opening. + */ + open(event = {}, columns) { + const menu = []; + const subMenu = { timings: [], responseHeaders: [] }; + const onlyOneColumn = getVisibleColumns(columns).length === 1; + + for (const column in columns) { + const shown = columns[column]; + const label = nonLocalizedHeaders.includes(column) + ? stringMap[column] || column + : L10N.getStr(`netmonitor.toolbar.${stringMap[column] || column}`); + const entry = { + id: `request-list-header-${column}-toggle`, + label, + type: "checkbox", + checked: shown, + click: () => this.props.toggleColumn(column), + // We don't want to allow hiding the last visible column + disabled: onlyOneColumn && shown, + }; + subMenuMap.hasOwnProperty(column) + ? subMenu[subMenuMap[column]].push(entry) + : menu.push(entry); + } + + menu.push({ type: "separator" }); + menu.push({ + label: L10N.getStr("netmonitor.toolbar.timings"), + submenu: subMenu.timings, + }); + menu.push({ + label: L10N.getStr("netmonitor.toolbar.responseHeaders"), + submenu: subMenu.responseHeaders, + }); + + menu.push({ type: "separator" }); + menu.push({ + id: "request-list-header-reset-columns", + label: L10N.getStr("netmonitor.toolbar.resetColumns"), + click: () => this.props.resetColumns(), + }); + + menu.push({ + id: "request-list-header-reset-sorting", + label: L10N.getStr("netmonitor.toolbar.resetSorting"), + click: () => this.props.resetSorting(), + }); + + const columnName = event.target.getAttribute("data-name"); + + menu.push({ + id: "request-list-header-resize-column-to-fit-content", + label: L10N.getStr("netmonitor.toolbar.resizeColumnToFitContent"), + click: () => this.props.resizeColumnToFitContent(columnName), + }); + + showMenu(menu, { + screenX: event.screenX, + screenY: event.screenY, + }); + } +} + +module.exports = RequestListHeaderContextMenu; diff --git a/devtools/client/netmonitor/src/widgets/WaterfallBackground.js b/devtools/client/netmonitor/src/widgets/WaterfallBackground.js new file mode 100644 index 0000000000..e2be7f5715 --- /dev/null +++ b/devtools/client/netmonitor/src/widgets/WaterfallBackground.js @@ -0,0 +1,163 @@ +/* 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 { getColor } = require("resource://devtools/client/shared/theme.js"); +const { colorUtils } = require("resource://devtools/shared/css/color.js"); +const { + REQUESTS_WATERFALL, +} = require("resource://devtools/client/netmonitor/src/constants.js"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const STATE_KEYS = [ + "firstRequestStartedMs", + "scale", + "timingMarkers", + "waterfallWidth", +]; + +/** + * Creates the background displayed on each waterfall view in this container. + */ +class WaterfallBackground { + constructor() { + this.canvas = document.createElementNS(HTML_NS, "canvas"); + this.ctx = this.canvas.getContext("2d"); + this.prevState = {}; + } + + /** + * Changes the element being used as the CSS background for a background + * with a given background element ID. + * + * The funtion wrap the Firefox only API. Waterfall Will not draw the + * vertical line when running on non-firefox browser. + * Could be fixed by Bug 1308695 + */ + setImageElement(imageElementId, imageElement) { + if (document.mozSetImageElement) { + document.mozSetImageElement(imageElementId, imageElement); + } + } + + draw(state) { + // Do a shallow compare of the previous and the new state + const shouldUpdate = STATE_KEYS.some( + key => this.prevState[key] !== state[key] + ); + if (!shouldUpdate) { + return; + } + + this.prevState = state; + + if (state.waterfallWidth === null || state.scale === null) { + this.setImageElement("waterfall-background", null); + return; + } + + // Nuke the context. + const canvasWidth = (this.canvas.width = Math.max( + state.waterfallWidth - REQUESTS_WATERFALL.LABEL_WIDTH, + 1 + )); + // Awww yeah, 1px, repeats on Y axis. + const canvasHeight = (this.canvas.height = 1); + + // Start over. + const imageData = this.ctx.createImageData(canvasWidth, canvasHeight); + const pixelArray = imageData.data; + + const buf = new ArrayBuffer(pixelArray.length); + const view8bit = new Uint8ClampedArray(buf); + const view32bit = new Uint32Array(buf); + + // Build new millisecond tick lines... + let timingStep = REQUESTS_WATERFALL.BACKGROUND_TICKS_MULTIPLE; + let optimalTickIntervalFound = false; + let scaledStep; + + while (!optimalTickIntervalFound) { + // Ignore any divisions that would end up being too close to each other. + scaledStep = state.scale * timingStep; + if (scaledStep < REQUESTS_WATERFALL.BACKGROUND_TICKS_SPACING_MIN) { + timingStep <<= 1; + continue; + } + optimalTickIntervalFound = true; + } + + const isRTL = document.dir === "rtl"; + const [r, g, b] = REQUESTS_WATERFALL.BACKGROUND_TICKS_COLOR_RGB; + let alphaComponent = REQUESTS_WATERFALL.BACKGROUND_TICKS_OPACITY_MIN; + + function drawPixelAt(offset, color) { + const position = (isRTL ? canvasWidth - offset : offset - 1) | 0; + const [rc, gc, bc, ac] = color; + view32bit[position] = (ac << 24) | (bc << 16) | (gc << 8) | rc; + } + + // Insert one pixel for each division on each scale. + for (let i = 1; i <= REQUESTS_WATERFALL.BACKGROUND_TICKS_SCALES; i++) { + const increment = scaledStep * Math.pow(2, i); + for (let x = 0; x < canvasWidth; x += increment) { + drawPixelAt(x, [r, g, b, alphaComponent]); + } + alphaComponent += REQUESTS_WATERFALL.BACKGROUND_TICKS_OPACITY_ADD; + } + + function drawTimestamp(timestamp, color) { + if (timestamp === -1) { + return; + } + + const delta = Math.floor( + (timestamp - state.firstRequestStartedMs) * state.scale + ); + drawPixelAt(delta, color); + } + + const { DOMCONTENTLOADED_TICKS_COLOR, LOAD_TICKS_COLOR } = + REQUESTS_WATERFALL; + drawTimestamp( + state.timingMarkers.firstDocumentDOMContentLoadedTimestamp, + this.getThemeColorAsRgba(DOMCONTENTLOADED_TICKS_COLOR, state.theme) + ); + + drawTimestamp( + state.timingMarkers.firstDocumentLoadTimestamp, + this.getThemeColorAsRgba(LOAD_TICKS_COLOR, state.theme) + ); + + // Flush the image data and cache the waterfall background. + pixelArray.set(view8bit); + this.ctx.putImageData(imageData, 0, 0); + + this.setImageElement("waterfall-background", this.canvas); + } + + /** + * Retrieve a color defined for the provided theme as a rgba array. The alpha channel is + * forced to the waterfall constant TICKS_COLOR_OPACITY. + * + * @param {String} colorName + * The name of the theme color + * @param {String} theme + * The name of the theme + * @return {Array} RGBA array for the color. + */ + getThemeColorAsRgba(colorName, theme) { + const colorStr = getColor(colorName, theme); + const color = new colorUtils.CssColor(colorStr); + const { r, g, b } = color.getRGBATuple(); + return [r, g, b, REQUESTS_WATERFALL.TICKS_COLOR_OPACITY]; + } + + destroy() { + this.setImageElement("waterfall-background", null); + } +} + +module.exports = WaterfallBackground; diff --git a/devtools/client/netmonitor/src/widgets/moz.build b/devtools/client/netmonitor/src/widgets/moz.build new file mode 100644 index 0000000000..106deca935 --- /dev/null +++ b/devtools/client/netmonitor/src/widgets/moz.build @@ -0,0 +1,12 @@ +# 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( + "HeadersPanelContextMenu.js", + "PropertiesViewContextMenu.js", + "RequestBlockingContextMenu.js", + "RequestListContextMenu.js", + "RequestListHeaderContextMenu.js", + "WaterfallBackground.js", +) -- cgit v1.2.3