// Copyright 2022 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import '../../ui/kit/kit.js'; import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Persistence from '../../models/persistence/persistence.js'; import * as Workspace from '../../models/workspace/workspace.js'; import * as NetworkForward from '../../panels/network/forward/forward.js'; import * as Input from '../../ui/components/input/input.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Lit from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import * as Sources from '../sources/sources.js'; import * as NetworkComponents from './components/components.js'; import {ShowMoreDetailsWidget} from './ShowMoreDetailsWidget.js'; const {render, html} = Lit; const UIStrings = { /** * @description Text in Request Headers View of the Network panel */ fromDiskCache: '(from disk cache)', /** * @description Text in Request Headers View of the Network panel */ fromMemoryCache: '(from memory cache)', /** * @description Text in Request Headers View of the Network panel */ fromEarlyHints: '(from early hints)', /** * @description Text in Request Headers View of the Network panel */ fromPrefetchCache: '(from prefetch cache)', /** * @description Text in Request Headers View of the Network panel */ fromServiceWorker: '(from `service worker`)', /** * @description Text in Request Headers View of the Network panel */ fromSignedexchange: '(from signed-exchange)', /** * @description Section header for a list of the main aspects of a http request */ general: 'General', /** * @description Label for a checkbox to switch between raw and parsed headers */ raw: 'Raw', /** * @description Text in Request Headers View of the Network panel */ referrerPolicy: 'Referrer Policy', /** * @description Text in Network Log View Columns of the Network panel */ remoteAddress: 'Remote Address', /** * @description Text in Request Headers View of the Network panel */ requestHeaders: 'Request Headers', /** * @description The HTTP method of a request */ requestMethod: 'Request Method', /** * @description The URL of a request */ requestUrl: 'Request URL', /** * @description A context menu item in the Network Log View Columns of the Network panel */ responseHeaders: 'Response headers', /** * @description A context menu item in the Network Log View Columns of the Network panel */ earlyHintsHeaders: 'Early hints headers', /** * @description Title text for a link to the Sources panel to the file containing the header override definitions */ revealHeaderOverrides: 'Reveal header override definitions', /** * @description HTTP response code */ statusCode: 'Status Code', } as const; const str_ = i18n.i18n.registerUIStrings('panels/network/RequestHeadersView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); interface ViewInput { showRequestHeadersText: boolean; showResponseHeadersText: boolean; request: SDK.NetworkRequest.NetworkRequest; toggleShowRawResponseHeaders: () => void; toggleShowRawRequestHeaders: () => void; revealHeadersFile?: () => void; toReveal?: {section: NetworkForward.UIRequestLocation.UIHeaderSection, header?: string}; } type View = (input: ViewInput, output: object, target: HTMLElement) => void; export const DEFAULT_VIEW: View = (input, output, target) => { const requestHeadersText = input.request.requestHeadersText(); const statusClasses = ['status']; if (input.request.statusCode < 300 || input.request.statusCode === 304) { statusClasses.push('green-circle'); } else if (input.request.statusCode < 400) { statusClasses.push('yellow-circle'); } else { statusClasses.push('red-circle'); } let comment = ''; if (input.request.cachedInMemory()) { comment = i18nString(UIStrings.fromMemoryCache); } else if (input.request.fromEarlyHints()) { comment = i18nString(UIStrings.fromEarlyHints); } else if (input.request.fetchedViaServiceWorker) { comment = i18nString(UIStrings.fromServiceWorker); } else if (input.request.redirectSourceSignedExchangeInfoHasNoErrors()) { comment = i18nString(UIStrings.fromSignedexchange); } else if (input.request.fromPrefetchCache()) { comment = i18nString(UIStrings.fromPrefetchCache); } else if (input.request.cached()) { comment = i18nString(UIStrings.fromDiskCache); } if (comment) { statusClasses.push('status-with-comment'); } const statusText = [input.request.statusCode, input.request.getInferredStatusText(), comment].join(' '); // clang-format off render( html` ${renderCategory({ name: 'general', title: i18nString(UIStrings.general), forceOpen: input.toReveal?.section === NetworkForward.UIRequestLocation.UIHeaderSection.GENERAL, loggingContext: 'general', contents: html`
${renderGeneralRow(input, i18nString(UIStrings.requestUrl), input.request.url(), 'request-url')} ${input.request.statusCode? renderGeneralRow(input, i18nString(UIStrings.requestMethod), input.request.requestMethod, 'request-method') : Lit.nothing} ${input.request.statusCode? renderGeneralRow(input, i18nString(UIStrings.statusCode), statusText, 'status-code', statusClasses) : Lit.nothing} ${input.request.remoteAddress()? renderGeneralRow(input, i18nString(UIStrings.remoteAddress), input.request.remoteAddress(), 'remote-address') : Lit.nothing} ${input.request.referrerPolicy()? renderGeneralRow(input, i18nString(UIStrings.referrerPolicy), String(input.request.referrerPolicy()), 'referrer-policy') : Lit.nothing}
` })} ${!input.request?.earlyHintsHeaders || input.request.earlyHintsHeaders.length === 0 ? Lit.nothing : renderCategory({ name: 'early-hints-headers', onToggleRawHeaders: input.toggleShowRawResponseHeaders, title: i18nString(UIStrings.earlyHintsHeaders), headerCount: input.request.earlyHintsHeaders.length, checked: undefined, additionalContent: undefined, forceOpen: input.toReveal?.section === NetworkForward.UIRequestLocation.UIHeaderSection.EARLY_HINTS, loggingContext: 'early-hints-headers', contents: input.showResponseHeadersText ? renderRawHeaders(input.request.responseHeadersText) : html` ` })} ${renderCategory({ name: 'response-headers', onToggleRawHeaders: input.toggleShowRawResponseHeaders, title: i18nString(UIStrings.responseHeaders), headerCount: input.request.sortedResponseHeaders.length, checked: input.request.responseHeadersText ? input.showResponseHeadersText : undefined, additionalContent: renderHeaderOverridesLink(input), forceOpen: input.toReveal?.section === NetworkForward.UIRequestLocation.UIHeaderSection.RESPONSE, loggingContext: 'response-headers', contents: input.showResponseHeadersText ? renderRawHeaders(input.request.responseHeadersText) : html` ` })} ${renderCategory({ name: 'request-headers', onToggleRawHeaders: input.toggleShowRawRequestHeaders, title: i18nString(UIStrings.requestHeaders), headerCount: input.request.requestHeaders().length, checked: requestHeadersText ? input.showRequestHeadersText : undefined, forceOpen: input.toReveal?.section === NetworkForward.UIRequestLocation.UIHeaderSection.REQUEST, loggingContext: 'request-headers', contents: (input.showRequestHeadersText && requestHeadersText) ? renderRawHeaders(requestHeadersText) : html` ` })} `, // clang-format on target); }; export class RequestHeadersView extends UI.Widget.Widget { #request?: SDK.NetworkRequest.NetworkRequest; #showResponseHeadersText = false; #showRequestHeadersText = false; #toReveal?: {section: NetworkForward.UIRequestLocation.UIHeaderSection, header?: string} = undefined; readonly #workspace = Workspace.Workspace.WorkspaceImpl.instance(); #view: View; get request(): SDK.NetworkRequest.NetworkRequest|undefined { return this.#request; } set request(val) { this.#removeEventListeners(); this.#request = val; this.#addEventListeners(); } constructor(target?: HTMLElement, view = DEFAULT_VIEW) { super({jslog: `${VisualLogging.pane('headers').track({resize: true})}`}); this.#view = view; } #addEventListeners(): void { this.#request?.addEventListener(SDK.NetworkRequest.Events.REMOTE_ADDRESS_CHANGED, this.#refreshHeadersView, this); this.#request?.addEventListener(SDK.NetworkRequest.Events.FINISHED_LOADING, this.#refreshHeadersView, this); this.#request?.addEventListener(SDK.NetworkRequest.Events.REQUEST_HEADERS_CHANGED, this.#refreshHeadersView, this); this.#request?.addEventListener( SDK.NetworkRequest.Events.RESPONSE_HEADERS_CHANGED, this.#resetAndRefreshHeadersView, this); this.#workspace.addEventListener( Workspace.Workspace.Events.UISourceCodeAdded, this.#uiSourceCodeAddedOrRemoved, this); this.#workspace.addEventListener( Workspace.Workspace.Events.UISourceCodeRemoved, this.#uiSourceCodeAddedOrRemoved, this); Common.Settings.Settings.instance() .moduleSetting('persistence-network-overrides-enabled') .addChangeListener(this.requestUpdate, this); } override wasShown(): void { super.wasShown(); this.#addEventListeners(); this.#toReveal = undefined; this.#refreshHeadersView(); } override willHide(): void { super.willHide(); this.#removeEventListeners(); } #removeEventListeners(): void { this.#request?.removeEventListener( SDK.NetworkRequest.Events.REMOTE_ADDRESS_CHANGED, this.#refreshHeadersView, this); this.#request?.removeEventListener(SDK.NetworkRequest.Events.FINISHED_LOADING, this.#refreshHeadersView, this); this.#request?.removeEventListener( SDK.NetworkRequest.Events.REQUEST_HEADERS_CHANGED, this.#refreshHeadersView, this); this.#request?.removeEventListener( SDK.NetworkRequest.Events.RESPONSE_HEADERS_CHANGED, this.#resetAndRefreshHeadersView, this); this.#workspace.removeEventListener( Workspace.Workspace.Events.UISourceCodeAdded, this.#uiSourceCodeAddedOrRemoved, this); this.#workspace.removeEventListener( Workspace.Workspace.Events.UISourceCodeRemoved, this.#uiSourceCodeAddedOrRemoved, this); Common.Settings.Settings.instance() .moduleSetting('persistence-network-overrides-enabled') .removeChangeListener(this.requestUpdate, this); } #resetAndRefreshHeadersView(): void { this.#request?.deleteAssociatedData(NetworkComponents.ResponseHeaderSection.RESPONSE_HEADER_SECTION_DATA_KEY); this.requestUpdate(); } #refreshHeadersView(): void { this.requestUpdate(); } revealHeader(section: NetworkForward.UIRequestLocation.UIHeaderSection, header?: string): void { this.#toReveal = {section, header}; this.requestUpdate(); } #uiSourceCodeAddedOrRemoved(event: Common.EventTarget.EventTargetEvent): void { if (this.#getHeaderOverridesFileUrl() === event.data.url()) { this.requestUpdate(); } } override performUpdate(): void { if (!this.#request) { return; } let revealHeadersFile; const uiSourceCode = this.#workspace.uiSourceCodeForURL(this.#getHeaderOverridesFileUrl()); if (uiSourceCode) { revealHeadersFile = (): void => { Sources.SourcesPanel.SourcesPanel.instance().showUISourceCode(uiSourceCode); void Sources.SourcesPanel.SourcesPanel.instance().revealInNavigator(uiSourceCode); }; } const input: ViewInput = { toggleShowRawResponseHeaders: (): void => { this.#showResponseHeadersText = !this.#showResponseHeadersText; this.requestUpdate(); }, toggleShowRawRequestHeaders: (): void => { this.#showRequestHeadersText = !this.#showRequestHeadersText; this.requestUpdate(); }, revealHeadersFile, request: this.#request, toReveal: this.#toReveal, showResponseHeadersText: this.#showResponseHeadersText, showRequestHeadersText: this.#showRequestHeadersText, }; this.#view(input, {}, this.contentElement); } #getHeaderOverridesFileUrl(): Platform.DevToolsPath.UrlString { if (!this.#request) { return Platform.DevToolsPath.EmptyUrlString; } const fileUrl = Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().fileUrlFromNetworkUrl( this.#request.url(), /* ignoreInactive */ true); return fileUrl.substring(0, fileUrl.lastIndexOf('/')) + '/' + Persistence.NetworkPersistenceManager.HEADERS_FILENAME as Platform.DevToolsPath.UrlString; } } function renderHeaderOverridesLink(input: ViewInput): Lit.LitTemplate { if (!input.revealHeadersFile) { return Lit.nothing; } const revealHeadersFile = (event: Event): void => { event.preventDefault(); input.revealHeadersFile?.(); }; const overridesSetting: Common.Settings.Setting = Common.Settings.Settings.instance().moduleSetting('persistence-network-overrides-enabled'); // Disabled until https://crbug.com/1079231 is fixed. // clang-format off const fileIcon = html` `; // clang-format on // Disabled until https://crbug.com/1079231 is fixed. // clang-format off return html` ${fileIcon}${Persistence.NetworkPersistenceManager.HEADERS_FILENAME} `; // clang-format on } function renderRawHeaders(text: string): Lit.TemplateResult { return html`
`; } function renderGeneralRow( input: ViewInput, name: Common.UIString.LocalizedString, value: string, id: string, classNames?: string[]): Lit.LitTemplate { const isHighlighted = input.toReveal?.section === NetworkForward.UIRequestLocation.UIHeaderSection.GENERAL && name.toLowerCase() === input.toReveal?.header?.toLowerCase(); return html`
${name}
Host.userMetrics.actionTaken(Host.UserMetrics.Action.NetworkPanelCopyValue)} >${value}
`; } export function renderCategory(data: { name: string, title: Common.UIString.LocalizedString, contents: Lit.LitTemplate, loggingContext: string, headerCount?: number, checked?: boolean, additionalContent?: Lit.LitTemplate, forceOpen?: boolean, onToggleRawHeaders?: () => void, }): Lit.LitTemplate { const expandedSetting = Common.Settings.Settings.instance().createSetting('request-info-' + data.name + '-category-expanded', true); const isOpen = (expandedSetting ? expandedSetting.get() : true) || data.forceOpen; // Disabled until https://crbug.com/1079231 is fixed. // clang-format off return html`
${data.title}${data.headerCount !== undefined ? html` (${data.headerCount})` : Lit.nothing }
${data.checked !== undefined ? html` ${i18nString(UIStrings.raw)} ` : Lit.nothing}
${data.additionalContent}
${data.contents}
`; // clang-format on function onSummaryKeyDown(event: KeyboardEvent): void { if (!event.target) { return; } const summaryElement = event.target as HTMLElement; const detailsElement = summaryElement.parentElement as HTMLDetailsElement; if (!detailsElement) { throw new Error('
element is not found for a element'); } switch (event.key) { case 'ArrowLeft': detailsElement.open = false; break; case 'ArrowRight': detailsElement.open = true; break; } } function onToggle(event: Event): void { expandedSetting?.set((event.target as HTMLDetailsElement).open); } }