summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/netmonitor/src/widgets/RequestListContextMenu.js793
1 files changed, 793 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
new file mode 100644
index 0000000000..1702cdbc64
--- /dev/null
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -0,0 +1,793 @@
+/* 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,
+ 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-image-as",
+ label: L10N.getStr("netmonitor.context.saveImageAs"),
+ accesskey: L10N.getStr("netmonitor.context.saveImageAs.accesskey"),
+ visible: !!(
+ clickedRequest &&
+ (responseContentAvailable || responseContent) &&
+ mimeType &&
+ mimeType.includes("image/")
+ ),
+ click: () => this.saveImageAs(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 image as.
+ */
+ async saveImageAs(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;