// Copyright 2021 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. * Copyright (C) IBM Corp. 2009 All rights reserved. * Copyright (C) 2010 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 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 SDK from '../../core/sdk/sdk.js'; import * as Buttons from '../../ui/components/buttons/buttons.js'; import * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js'; // eslint-disable-next-line @devtools/es-modules-import import objectPropertiesSectionStyles from '../../ui/legacy/components/object_ui/objectPropertiesSection.css.js'; // eslint-disable-next-line @devtools/es-modules-import import objectValueStyles from '../../ui/legacy/components/object_ui/objectValue.css.js'; import * as UI from '../../ui/legacy/legacy.js'; import {Directives, html, type LitTemplate, render, type TemplateResult} from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import requestPayloadTreeStyles from './requestPayloadTree.css.js'; import requestPayloadViewStyles from './requestPayloadView.css.js'; import {ShowMoreDetailsWidget} from './ShowMoreDetailsWidget.js'; const {classMap} = Directives; const {widgetConfig} = UI.Widget; const {ifExpanded} = UI.TreeOutline; const UIStrings = { /** * @description A context menu item Payload View of the Network panel to copy a parsed value. */ copyValue: 'Copy value', /** * @description A context menu item Payload View of the Network panel to copy the payload. */ copyPayload: 'Copy', /** * @description Text in Request Payload View of the Network panel. This is a noun-phrase meaning the * payload of a network request. */ requestPayload: 'Request Payload', /** * @description Text in Request Payload View of the Network panel */ unableToDecodeValue: '(unable to decode value)', /** * @description Text in Request Payload View of the Network panel */ queryStringParameters: 'Query String Parameters', /** * @description Text in Request Payload View of the Network panel */ formData: 'Form Data', /** * @description Text for toggling the view of payload data (e.g. query string parameters) from source to parsed in the payload tab */ viewParsed: 'View parsed', /** * @description Text to show an item is empty */ empty: '(empty)', /** * @description Text for toggling the view of payload data (e.g. query string parameters) from parsed to source in the payload tab */ viewSource: 'View source', /** * @description Text for toggling payload data (e.g. query string parameters) from decoded to * encoded in the payload tab or in the cookies preview. URL-encoded is a different data format for * the same data, which the user sees when they click this command. */ viewUrlEncoded: 'View URL-encoded', /** * @description Text for toggling payload data (e.g. query string parameters) from encoded to decoded in the payload tab or in the cookies preview */ viewDecoded: 'View decoded', } as const; const str_ = i18n.i18n.registerUIStrings('panels/network/RequestPayloadView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); interface ViewInput { decodeRequestParameters: boolean; setURLDecoding(value: boolean): void; viewQueryParamSource: boolean; setViewQueryParamSource(value: boolean): void; viewFormParamSource: boolean; setViewFormParamSource(value: boolean): void; viewJSONPayloadSource: boolean; setViewJSONPayloadSource(value: boolean): void; copyValue(value: string): void; formData: string|undefined; formParameters: SDK.NetworkRequest.NameValue[]|undefined; queryString: string|null; queryParameters: SDK.NetworkRequest.NameValue[]|null; } type View = (input: ViewInput, output: object, target: HTMLElement) => void; export const DEFAULT_VIEW: View = (input, output, target) => { const createViewSourceToggle = (viewSource: boolean, callback: (value: boolean) => void): LitTemplate => html` { e.consume(); callback(!viewSource); }}> ${viewSource ? i18nString(UIStrings.viewParsed) : i18nString(UIStrings.viewSource)} `; const copyValueContextmenu = (title: string, value: () => string, jslogContext: string) => (e: Event) => { e.consume(true); const contextMenu = new UI.ContextMenu.ContextMenu(e); const copyValueHandler = (): void => input.copyValue(value()); contextMenu.clipboardSection().appendItem(title, copyValueHandler, {jslogContext}); void contextMenu.show(); }; const createSourceText = (text: string): TemplateResult => html`
  • text, 'copy-payload')}>
  • `; const createParsedParams = (params: SDK.NetworkRequest.NameValue[]): TemplateResult[] => params.map( param => html`
  • decodeURIComponent(param.value), 'copy-value')}>${ param.name !== '' ? html`${RequestPayloadView.formatParameter(param.name, 'payload-name', input.decodeRequestParameters)}${ RequestPayloadView.formatParameter( param.value, 'payload-value source-code', input.decodeRequestParameters)}` : RequestPayloadView.formatParameter( i18nString(UIStrings.empty), 'empty-request-payload', input.decodeRequestParameters)}
  • `); const parsedFormData = (() => { if (input.formData && !input.formParameters) { try { return JSON.parse(input.formData); } catch { } return undefined; } })(); const createPayload = (parsedFormData: unknown): TemplateResult => { const object = new SDK.RemoteObject.LocalJSONObject(parsedFormData); const section = new ObjectUI.ObjectPropertiesSection.RootElement(new ObjectUI.ObjectPropertiesSection.ObjectTree(object)); section.title = document.createTextNode(object.description); section.listItemElement.classList.add('source-code', 'object-properties-section'); section.childrenListElement.classList.add('source-code', 'object-properties-section'); section.expand(); return html``; }; const queryStringExpandedSetting = Common.Settings.Settings.instance().createSetting('request-info-query-string-category-expanded', true); const formDataExpandedSetting = Common.Settings.Settings.instance().createSetting('request-info-form-data-category-expanded', true); const requestPayloadExpandedSetting = Common.Settings.Settings.instance().createSetting('request-info-request-payload-category-expanded', true); const toggleURLDecoding = (e: Event): void => { e.consume(); input.setURLDecoding(!input.decodeRequestParameters); }; const onContextMenu = (viewSource: boolean, callback: (value: boolean) => void, includeURLDecodingOption = true) => ( event: Event): void => { const contextMenu = new UI.ContextMenu.ContextMenu(event); const section = contextMenu.newSection(); if (viewSource) { section.appendItem(i18nString(UIStrings.viewParsed), () => callback(!viewSource), {jslogContext: 'view-parsed'}); } else { section.appendItem(i18nString(UIStrings.viewSource), () => callback(!viewSource), {jslogContext: 'view-source'}); if (includeURLDecodingOption) { const viewURLEncodedText = input.decodeRequestParameters ? i18nString(UIStrings.viewUrlEncoded) : i18nString(UIStrings.viewDecoded); section.appendItem( viewURLEncodedText, toggleURLDecoding.bind(this, event), {jslogContext: 'toggle-url-decoding'}); } } void contextMenu.show(); }; // clang-format off render(html` ${objectValueStyles} `}> `, target); // clang-format on }; export class RequestPayloadView extends UI.Widget.VBox { #request?: SDK.NetworkRequest.NetworkRequest; #decodeRequestParameters = true; #formData?: string; #formParameters?: SDK.NetworkRequest.NameValue[]; #view: View; #viewJSONPayloadSource = false; #viewFormParamSource = false; #viewQueryParamSource = false; constructor(target?: HTMLElement, view = DEFAULT_VIEW) { super({jslog: `${VisualLogging.pane('payload').track({resize: true})}`, classes: ['request-payload-view']}); this.#view = view; } set request(request: SDK.NetworkRequest.NetworkRequest) { if (this.#request) { this.#request.removeEventListener(SDK.NetworkRequest.Events.REQUEST_HEADERS_CHANGED, this.#refreshFormData, this); } this.#request = request; const contentType = request.requestContentType(); if (contentType) { this.#decodeRequestParameters = Boolean(contentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)); } if (this.isShowing()) { this.#request?.addEventListener(SDK.NetworkRequest.Events.REQUEST_HEADERS_CHANGED, this.#refreshFormData, this); } this.requestUpdate(); void this.#refreshFormData(); } get request(): SDK.NetworkRequest.NetworkRequest|undefined { return this.#request; } override wasShown(): void { super.wasShown(); this.request?.addEventListener(SDK.NetworkRequest.Events.REQUEST_HEADERS_CHANGED, this.#refreshFormData, this); void this.#refreshFormData(); } override willHide(): void { super.willHide(); this.request?.removeEventListener(SDK.NetworkRequest.Events.REQUEST_HEADERS_CHANGED, this.#refreshFormData, this); } private addEntryContextMenuHandler( treeElement: UI.TreeOutline.TreeElement, menuItem: string, jslogContext: string, getValue: () => string): void { treeElement.listItemElement.addEventListener('contextmenu', event => { event.consume(true); const contextMenu = new UI.ContextMenu.ContextMenu(event); const copyValueHandler = (): void => { Host.userMetrics.actionTaken(Host.UserMetrics.Action.NetworkPanelCopyValue); Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(getValue()); }; contextMenu.clipboardSection().appendItem(menuItem, copyValueHandler, {jslogContext}); void contextMenu.show(); }); } override performUpdate(): void { if (!this.request) { return; } const input: ViewInput = { queryString: this.request.queryString(), queryParameters: this.request.queryParameters, formData: this.#formData, formParameters: this.#formParameters, decodeRequestParameters: this.#decodeRequestParameters, setURLDecoding: (value: boolean): void => { this.#decodeRequestParameters = value; this.requestUpdate(); }, viewQueryParamSource: this.#viewQueryParamSource, setViewQueryParamSource: (value: boolean): void => { this.#viewQueryParamSource = value; this.requestUpdate(); }, viewFormParamSource: this.#viewFormParamSource, setViewFormParamSource: (value: boolean): void => { this.#viewFormParamSource = value; this.requestUpdate(); }, viewJSONPayloadSource: this.#viewJSONPayloadSource, setViewJSONPayloadSource: (value: boolean): void => { this.#viewJSONPayloadSource = value; this.requestUpdate(); }, copyValue: (value: string): void => { Host.userMetrics.actionTaken(Host.UserMetrics.Action.NetworkPanelCopyValue); Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(value); } }; this.#view(input, {}, this.element); } async #refreshFormData(): Promise { this.#formData = await this.request?.requestFormData() ?? undefined; if (this.#formData) { this.#formParameters = await this.request?.formParameters() ?? undefined; } this.requestUpdate(); } static formatParameter(value: string, className: string, decodeParameters: boolean): LitTemplate { let errorDecoding = false; if (decodeParameters) { value = value.replace(/\+/g, ' '); if (value.indexOf('%') >= 0) { try { value = decodeURIComponent(value); } catch { errorDecoding = true; } } } const classes = classMap({[className]: !!className, 'empty-value': value === ''}); return html`
    ${ errorDecoding ? html`${i18nString(UIStrings.unableToDecodeValue)}` : value}
    `; } }