From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../src/components/request-details/CachePanel.js | 144 ++++ .../src/components/request-details/CookiesPanel.js | 225 ++++++ .../src/components/request-details/HeadersPanel.js | 850 +++++++++++++++++++++ .../request-details/NetworkDetailsBar.js | 106 +++ .../components/request-details/PropertiesView.js | 247 ++++++ .../src/components/request-details/RequestPanel.js | 301 ++++++++ .../components/request-details/ResponsePanel.js | 496 ++++++++++++ .../components/request-details/SecurityPanel.js | 283 +++++++ .../components/request-details/StackTracePanel.js | 81 ++ .../src/components/request-details/TimingsPanel.js | 229 ++++++ .../src/components/request-details/moz.build | 16 + 11 files changed, 2978 insertions(+) create mode 100644 devtools/client/netmonitor/src/components/request-details/CachePanel.js create mode 100644 devtools/client/netmonitor/src/components/request-details/CookiesPanel.js create mode 100644 devtools/client/netmonitor/src/components/request-details/HeadersPanel.js create mode 100644 devtools/client/netmonitor/src/components/request-details/NetworkDetailsBar.js create mode 100644 devtools/client/netmonitor/src/components/request-details/PropertiesView.js create mode 100644 devtools/client/netmonitor/src/components/request-details/RequestPanel.js create mode 100644 devtools/client/netmonitor/src/components/request-details/ResponsePanel.js create mode 100644 devtools/client/netmonitor/src/components/request-details/SecurityPanel.js create mode 100644 devtools/client/netmonitor/src/components/request-details/StackTracePanel.js create mode 100644 devtools/client/netmonitor/src/components/request-details/TimingsPanel.js create mode 100644 devtools/client/netmonitor/src/components/request-details/moz.build (limited to 'devtools/client/netmonitor/src/components/request-details') diff --git a/devtools/client/netmonitor/src/components/request-details/CachePanel.js b/devtools/client/netmonitor/src/components/request-details/CachePanel.js new file mode 100644 index 0000000000..5f3db6c501 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-details/CachePanel.js @@ -0,0 +1,144 @@ +/* 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 { + Component, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const { + fetchNetworkUpdatePacket, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +// Components +const TreeViewClass = require("resource://devtools/client/shared/components/tree/TreeView.js"); +const PropertiesView = createFactory( + require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js") +); + +const { div, input } = dom; + +const CACHE = L10N.getStr("netmonitor.cache.cache"); +const DATA_SIZE = L10N.getStr("netmonitor.cache.dataSize"); +const EXPIRES = L10N.getStr("netmonitor.cache.expires"); +const FETCH_COUNT = L10N.getStr("netmonitor.cache.fetchCount"); +const LAST_FETCHED = L10N.getStr("netmonitor.cache.lastFetched"); +const LAST_MODIFIED = L10N.getStr("netmonitor.cache.lastModified"); +const DEVICE = L10N.getStr("netmonitor.cache.device"); +const NOT_AVAILABLE = L10N.getStr("netmonitor.cache.notAvailable"); +const EMPTY = L10N.getStr("netmonitor.cache.empty"); + +/** + * Cache panel component + * This tab lists full details of any cache information of the response. + */ +class CachePanel extends Component { + static get propTypes() { + return { + connector: PropTypes.object.isRequired, + openLink: PropTypes.func, + request: PropTypes.object.isRequired, + }; + } + + componentDidMount() { + const { connector, request } = this.props; + fetchNetworkUpdatePacket(connector.requestData, request, ["responseCache"]); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + const { connector, request } = nextProps; + fetchNetworkUpdatePacket(connector.requestData, request, ["responseCache"]); + } + + renderSummary(label, value) { + return div( + { className: "tabpanel-summary-container cache-summary" }, + div( + { + className: "tabpanel-summary-label cache-summary-label", + }, + label + ":" + ), + input({ + className: "tabpanel-summary-value textbox-input devtools-monospace", + readOnly: true, + value, + }) + ); + } + + getProperties(responseCache) { + let responseCacheObj; + let cacheObj; + try { + responseCacheObj = Object.assign({}, responseCache); + responseCacheObj = responseCacheObj.cache; + } catch (e) { + return null; + } + try { + cacheObj = Object.assign({}, responseCacheObj); + } catch (e) { + return null; + } + return cacheObj; + } + + getDate(timestamp) { + if (!timestamp) { + return null; + } + const d = new Date(parseInt(timestamp, 10) * 1000); + return d.toLocaleDateString() + " " + d.toLocaleTimeString(); + } + + render() { + const { request } = this.props; + const { responseCache } = request; + + let object; + const cache = this.getProperties(responseCache); + + if ( + cache.lastFetched || + cache.fetchCount || + cache.storageDataSize || + cache.lastModified || + cache.expirationTime || + cache.deviceID + ) { + object = { + [CACHE]: { + [LAST_FETCHED]: this.getDate(cache.lastFetched) || NOT_AVAILABLE, + [FETCH_COUNT]: cache.fetchCount || NOT_AVAILABLE, + [DATA_SIZE]: cache.storageDataSize || NOT_AVAILABLE, + [LAST_MODIFIED]: this.getDate(cache.lastModified) || NOT_AVAILABLE, + [EXPIRES]: this.getDate(cache.expirationTime) || NOT_AVAILABLE, + [DEVICE]: cache.deviceID || NOT_AVAILABLE, + }, + }; + } else { + return div({ className: "empty-notice" }, EMPTY); + } + + return div( + { className: "panel-container security-panel" }, + PropertiesView({ + object, + enableFilter: false, + expandedNodes: TreeViewClass.getExpandedNodes(object), + }) + ); + } +} + +module.exports = CachePanel; diff --git a/devtools/client/netmonitor/src/components/request-details/CookiesPanel.js b/devtools/client/netmonitor/src/components/request-details/CookiesPanel.js new file mode 100644 index 0000000000..466c0379cd --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-details/CookiesPanel.js @@ -0,0 +1,225 @@ +/* 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 { + Component, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const { + fetchNetworkUpdatePacket, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const { + sortObjectKeys, +} = require("resource://devtools/client/netmonitor/src/utils/sort-utils.js"); +const { + FILTER_SEARCH_DELAY, +} = require("resource://devtools/client/netmonitor/src/constants.js"); + +// Component +const PropertiesView = createFactory( + require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js") +); +const SearchBox = createFactory( + require("resource://devtools/client/shared/components/SearchBox.js") +); +const Accordion = createFactory( + require("resource://devtools/client/shared/components/Accordion.js") +); + +loader.lazyGetter(this, "TreeRow", function () { + return createFactory( + require("resource://devtools/client/shared/components/tree/TreeRow.js") + ); +}); + +const { div } = dom; + +const COOKIES_EMPTY_TEXT = L10N.getStr("cookiesEmptyText"); +const COOKIES_FILTER_TEXT = L10N.getStr("cookiesFilterText"); +const REQUEST_COOKIES = L10N.getStr("requestCookies"); +const RESPONSE_COOKIES = L10N.getStr("responseCookies"); + +/* + * Cookies panel component + * This tab lists full details of any cookies sent with the request or response + */ +class CookiesPanel extends Component { + static get propTypes() { + return { + connector: PropTypes.object.isRequired, + openLink: PropTypes.func, + request: PropTypes.object.isRequired, + targetSearchResult: PropTypes.object, + }; + } + + constructor(props) { + super(props); + this.state = { + filterText: "", + }; + } + + componentDidMount() { + const { connector, request } = this.props; + fetchNetworkUpdatePacket(connector.requestData, request, [ + "requestCookies", + "responseCookies", + ]); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + const { connector, request } = nextProps; + fetchNetworkUpdatePacket(connector.requestData, request, [ + "requestCookies", + "responseCookies", + ]); + } + + /** + * Mapping array to dict for TreeView usage. + * Since TreeView only support Object(dict) format. + * + * @param {Object[]} arr - key-value pair array like cookies or params + * @returns {Object} + */ + getProperties(arr, title) { + const cookies = arr.reduce((map, obj) => { + // Generally cookies object contains only name and value properties and can + // be rendered as name: value pair. + // When there are more properties in cookies object such as extra or path, + // We will pass the object to display these extra information + if (Object.keys(obj).length > 2) { + map[obj.name] = Object.assign({}, obj); + delete map[obj.name].name; + } else { + map[obj.name] = obj.value; + } + return map; + }, Object.create(null)); + + // To have different roots for Request and Response cookies + return { [title]: cookies }; + } + + /** + * Custom rendering method passed to PropertiesView. It's + * responsible to filter out level 0 node in the tree + * + * @param {Object} props + */ + renderRow(props) { + const { level } = props.member; + + if (level === 0) { + return null; + } + + return TreeRow(props); + } + + /** + * Get the selected cookies path + * @param {Object} searchResult + * @returns {string} + */ + getTargetCookiePath(searchResult) { + if (!searchResult) { + return null; + } + + switch (searchResult.type) { + case "requestCookies": { + return `/${REQUEST_COOKIES}/${searchResult.label}`; + } + case "responseCookies": + return `/${RESPONSE_COOKIES}/${searchResult.label}`; + } + + return null; + } + + render() { + let { + request: { + requestCookies = { cookies: [] }, + responseCookies = { cookies: [] }, + }, + targetSearchResult, + } = this.props; + + const { filterText } = this.state; + + requestCookies = requestCookies.cookies || requestCookies; + responseCookies = responseCookies.cookies || responseCookies; + + if (!requestCookies.length && !responseCookies.length) { + return div({ className: "empty-notice" }, COOKIES_EMPTY_TEXT); + } + + const items = []; + + if (responseCookies.length) { + items.push({ + component: PropertiesView, + componentProps: { + object: sortObjectKeys( + this.getProperties(responseCookies, RESPONSE_COOKIES) + ), + filterText, + targetSearchResult, + defaultSelectFirstNode: false, + selectPath: this.getTargetCookiePath, + renderRow: this.renderRow, + }, + header: RESPONSE_COOKIES, + id: "responseCookies", + opened: true, + }); + } + + if (requestCookies.length) { + items.push({ + component: PropertiesView, + componentProps: { + object: sortObjectKeys( + this.getProperties(requestCookies, REQUEST_COOKIES) + ), + filterText, + targetSearchResult, + defaultSelectFirstNode: false, + selectPath: this.getTargetCookiePath, + renderRow: this.renderRow, + }, + header: REQUEST_COOKIES, + id: "requestCookies", + opened: true, + }); + } + + return div( + { className: "panel-container cookies-panel-container" }, + div( + { className: "devtools-toolbar devtools-input-toolbar" }, + SearchBox({ + delay: FILTER_SEARCH_DELAY, + type: "filter", + onChange: text => this.setState({ filterText: text }), + placeholder: COOKIES_FILTER_TEXT, + }) + ), + Accordion({ items }) + ); + } +} + +module.exports = CookiesPanel; diff --git a/devtools/client/netmonitor/src/components/request-details/HeadersPanel.js b/devtools/client/netmonitor/src/components/request-details/HeadersPanel.js new file mode 100644 index 0000000000..80b9aed27d --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-details/HeadersPanel.js @@ -0,0 +1,850 @@ +/* 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 { + Component, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + connect, +} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js"); +const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { + getFormattedIPAndPort, + getFormattedSize, + getRequestPriorityAsText, +} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const { + getHeadersURL, + getTrackingProtectionURL, + getHTTPStatusCodeURL, +} = require("resource://devtools/client/netmonitor/src/utils/doc-utils.js"); +const { + fetchNetworkUpdatePacket, + writeHeaderText, + getRequestHeadersRawText, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const { + HeadersProvider, + HeaderList, +} = require("resource://devtools/client/netmonitor/src/utils/headers-provider.js"); +const { + FILTER_SEARCH_DELAY, +} = require("resource://devtools/client/netmonitor/src/constants.js"); +// Components +const PropertiesView = createFactory( + require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js") +); +const SearchBox = createFactory( + require("resource://devtools/client/shared/components/SearchBox.js") +); +const Accordion = createFactory( + require("resource://devtools/client/shared/components/Accordion.js") +); +const UrlPreview = createFactory( + require("resource://devtools/client/netmonitor/src/components/previews/UrlPreview.js") +); +const HeadersPanelContextMenu = require("resource://devtools/client/netmonitor/src/widgets/HeadersPanelContextMenu.js"); +const StatusCode = createFactory( + require("resource://devtools/client/netmonitor/src/components/StatusCode.js") +); + +loader.lazyGetter(this, "MDNLink", function () { + return createFactory( + require("resource://devtools/client/shared/components/MdnLink.js") + ); +}); +loader.lazyGetter(this, "Rep", function () { + return require("resource://devtools/client/shared/components/reps/index.js") + .REPS.Rep; +}); +loader.lazyGetter(this, "MODE", function () { + return require("resource://devtools/client/shared/components/reps/index.js") + .MODE; +}); +loader.lazyGetter(this, "TreeRow", function () { + return createFactory( + require("resource://devtools/client/shared/components/tree/TreeRow.js") + ); +}); +loader.lazyRequireGetter( + this, + "showMenu", + "resource://devtools/client/shared/components/menu/utils.js", + true +); +loader.lazyRequireGetter( + this, + "openContentLink", + "resource://devtools/client/shared/link.js", + true +); + +const { div, input, label, span, textarea, tr, td, button } = dom; + +const RESEND = L10N.getStr("netmonitor.context.resend.label"); +const EDIT_AND_RESEND = L10N.getStr("netmonitor.summary.editAndResend"); +const RAW_HEADERS = L10N.getStr("netmonitor.headers.raw"); +const HEADERS_EMPTY_TEXT = L10N.getStr("headersEmptyText"); +const HEADERS_FILTER_TEXT = L10N.getStr("headersFilterText"); +const REQUEST_HEADERS = L10N.getStr("requestHeaders"); +const REQUEST_HEADERS_FROM_UPLOAD = L10N.getStr("requestHeadersFromUpload"); +const RESPONSE_HEADERS = L10N.getStr("responseHeaders"); +const HEADERS_STATUS = L10N.getStr("netmonitor.headers.status"); +const HEADERS_VERSION = L10N.getStr("netmonitor.headers.version"); +const HEADERS_TRANSFERRED = L10N.getStr("netmonitor.toolbar.transferred"); +const SUMMARY_STATUS_LEARN_MORE = L10N.getStr("netmonitor.summary.learnMore"); +const SUMMARY_ETP_LEARN_MORE = L10N.getStr( + "netmonitor.enhancedTrackingProtection.learnMore" +); +const HEADERS_REFERRER = L10N.getStr("netmonitor.headers.referrerPolicy"); +const HEADERS_CONTENT_BLOCKING = L10N.getStr( + "netmonitor.headers.contentBlocking" +); +const HEADERS_ETP = L10N.getStr( + "netmonitor.trackingResource.enhancedTrackingProtection" +); +const HEADERS_PRIORITY = L10N.getStr("netmonitor.headers.requestPriority"); + +/** + * Headers panel component + * Lists basic information about the request + * + * In http/2 all response headers are in small case. + * See: https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/request_details/index.html#response-headers + * RFC: https://tools.ietf.org/html/rfc7540#section-8.1.2 + */ +class HeadersPanel extends Component { + static get propTypes() { + return { + connector: PropTypes.object.isRequired, + cloneSelectedRequest: PropTypes.func.isRequired, + member: PropTypes.object, + request: PropTypes.object.isRequired, + renderValue: PropTypes.func, + openLink: PropTypes.func, + targetSearchResult: PropTypes.object, + openRequestBlockingAndAddUrl: PropTypes.func.isRequired, + openHTTPCustomRequestTab: PropTypes.func.isRequired, + cloneRequest: PropTypes.func, + sendCustomRequest: PropTypes.func, + shouldExpandPreview: PropTypes.bool, + setHeadersUrlPreviewExpanded: PropTypes.func, + }; + } + + constructor(props) { + super(props); + + this.state = { + rawRequestHeadersOpened: false, + rawResponseHeadersOpened: false, + rawUploadHeadersOpened: false, + lastToggledRawHeader: "", + filterText: null, + }; + + this.getProperties = this.getProperties.bind(this); + this.getTargetHeaderPath = this.getTargetHeaderPath.bind(this); + this.toggleRawResponseHeaders = this.toggleRawResponseHeaders.bind(this); + this.toggleRawRequestHeaders = this.toggleRawRequestHeaders.bind(this); + this.toggleRawUploadHeaders = this.toggleRawUploadHeaders.bind(this); + this.renderSummary = this.renderSummary.bind(this); + this.renderRow = this.renderRow.bind(this); + this.renderValue = this.renderValue.bind(this); + this.renderRawHeadersBtn = this.renderRawHeadersBtn.bind(this); + this.onShowResendMenu = this.onShowResendMenu.bind(this); + this.onShowHeadersContextMenu = this.onShowHeadersContextMenu.bind(this); + } + + componentDidMount() { + const { request, connector } = this.props; + fetchNetworkUpdatePacket(connector.requestData, request, [ + "requestHeaders", + "responseHeaders", + "requestPostData", + ]); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + const { request, connector } = nextProps; + fetchNetworkUpdatePacket(connector.requestData, request, [ + "requestHeaders", + "responseHeaders", + "requestPostData", + ]); + } + + getHeadersTitle(headers, title) { + let result = ""; + let preHeaderText = ""; + const { + responseHeaders, + requestHeaders, + httpVersion, + status, + statusText, + method, + urlDetails, + } = this.props.request; + if (headers?.headers.length) { + if (!headers.headersSize) { + if (title == RESPONSE_HEADERS) { + preHeaderText = `${httpVersion} ${status} ${statusText}`; + result = `${title} (${getFormattedSize( + writeHeaderText(responseHeaders.headers, preHeaderText).length, + 3 + )})`; + } else { + const hostHeader = requestHeaders.headers.find( + ele => ele.name === "Host" + ); + if (hostHeader) { + preHeaderText = `${method} ${ + urlDetails.url.split(hostHeader.value)[1] + } ${httpVersion}`; + } + result = `${title} (${getFormattedSize( + writeHeaderText(requestHeaders.headers, preHeaderText).length, + 3 + )})`; + } + } else { + result = `${title} (${getFormattedSize(headers.headersSize, 3)})`; + } + } + + return result; + } + + getProperties(headers, title) { + let propertiesResult; + + if (headers?.headers.length) { + const headerKey = this.getHeadersTitle(headers, title); + propertiesResult = { + [headerKey]: new HeaderList(headers.headers), + }; + if ( + (title === RESPONSE_HEADERS && this.state.rawResponseHeadersOpened) || + (title === REQUEST_HEADERS && this.state.rawRequestHeadersOpened) || + (title === REQUEST_HEADERS_FROM_UPLOAD && + this.state.rawUploadHeadersOpened) + ) { + propertiesResult = { + [headerKey]: { RAW_HEADERS_ID: headers.rawHeaders }, + }; + } + } + return propertiesResult; + } + + toggleRawResponseHeaders() { + this.setState({ + rawResponseHeadersOpened: !this.state.rawResponseHeadersOpened, + lastToggledRawHeader: "response", + }); + } + + toggleRawRequestHeaders() { + this.setState({ + rawRequestHeadersOpened: !this.state.rawRequestHeadersOpened, + lastToggledRawHeader: "request", + }); + } + + toggleRawUploadHeaders() { + this.setState({ + rawUploadHeadersOpened: !this.state.rawUploadHeadersOpened, + lastToggledRawHeader: "upload", + }); + } + + /** + * Helper method to identify what kind of raw header this is. + * Information is in the path variable + */ + getRawHeaderType(path) { + if (path.includes(RESPONSE_HEADERS)) { + return "RESPONSE"; + } + if (path.includes(REQUEST_HEADERS_FROM_UPLOAD)) { + return "UPLOAD"; + } + return "REQUEST"; + } + + /** + * Renders the top part of the headers detail panel - Summary. + */ + renderSummary(summaryLabel, value) { + return div( + { + key: summaryLabel, + className: "tabpanel-summary-container headers-summary", + }, + span( + { className: "tabpanel-summary-label headers-summary-label" }, + summaryLabel + ), + span({ className: "tabpanel-summary-value" }, value) + ); + } + + /** + * Get path for target header if it's set. It's used to select + * the header automatically within the tree of headers. + * Note that the target header is set by the Search panel. + */ + getTargetHeaderPath(searchResult) { + if (!searchResult) { + return null; + } + if ( + searchResult.type !== "requestHeaders" && + searchResult.type !== "responseHeaders" && + searchResult.type !== "requestHeadersFromUploadStream" + ) { + return null; + } + const { + request: { + requestHeaders, + requestHeadersFromUploadStream: uploadHeaders, + responseHeaders, + }, + } = this.props; + // Using `HeaderList` ensures that we'll get the same + // header index as it's used in the tree. + const getPath = (headers, title) => { + return ( + "/" + + this.getHeadersTitle(headers, title) + + "/" + + new HeaderList(headers.headers).headers.findIndex( + header => header.name == searchResult.label + ) + ); + }; + // Calculate target header path according to the header type. + switch (searchResult.type) { + case "requestHeaders": + return getPath(requestHeaders, REQUEST_HEADERS); + case "responseHeaders": + return getPath(responseHeaders, RESPONSE_HEADERS); + case "requestHeadersFromUploadStream": + return getPath(uploadHeaders, REQUEST_HEADERS_FROM_UPLOAD); + } + return null; + } + + /** + * Custom rendering method passed to PropertiesView. It's responsible + * for rendering