| // Copyright 2021 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as BrowserSDK from '../browser_sdk/browser_sdk.js'; |
| import * as Common from '../core/common/common.js'; |
| import * as i18n from '../core/i18n/i18n.js'; |
| import * as SDK from '../core/sdk/sdk.js'; |
| import * as ConsoleCounters from '../panels/console_counters/console_counters.js'; |
| import * as UI from '../ui/legacy/legacy.js'; |
| |
| import type {AggregatedIssue} from './IssueAggregator.js'; |
| import {Events as IssueAggregatorEvents, IssueAggregator} from './IssueAggregator.js'; |
| import {IssueView} from './IssueView.js'; |
| import {createIssueDescriptionFromMarkdown} from './MarkdownIssueDescription.js'; |
| |
| const UIStrings = { |
| /** |
| * @description Category title for a group of cross origin embedder policy (COEP) issues |
| */ |
| crossOriginEmbedderPolicy: 'Cross Origin Embedder Policy', |
| /** |
| * @description Category title for a group of mixed content issues |
| */ |
| mixedContent: 'Mixed Content', |
| /** |
| * @description Category title for a group of SameSite cookie issues |
| */ |
| samesiteCookie: 'SameSite Cookie', |
| /** |
| * @description Category title for a group of heavy ads issues |
| */ |
| heavyAds: 'Heavy Ads', |
| /** |
| * @description Category title for a group of content security policy (CSP) issues |
| */ |
| contentSecurityPolicy: 'Content Security Policy', |
| /** |
| * @description Category title for a group of trusted web activity issues |
| */ |
| trustedWebActivity: 'Trusted Web Activity', |
| /** |
| * @description Text for other types of items |
| */ |
| other: 'Other', |
| /** |
| * @description Category title for the different 'low text contrast' issues. Low text contrast refers |
| * to the difference between the color of a text and the background color where that text |
| * appears. |
| */ |
| lowTextContrast: 'Low Text Contrast', |
| /** |
| * @description Category title for the different 'Cross-Origin Resource Sharing' (CORS) issues. CORS |
| * refers to one origin (e.g 'a.com') loading resources from another origin (e.g. 'b.com'). |
| */ |
| cors: 'Cross Origin Resource Sharing', |
| /** |
| * @description Title for a checkbox which toggles grouping by category in the issues tab |
| */ |
| groupDisplayedIssuesUnder: 'Group displayed issues under associated categories', |
| /** |
| * @description Label for a checkbox which toggles grouping by category in the issues tab |
| */ |
| groupByCategory: 'Group by category', |
| /** |
| * @description Title for a checkbox. Whether the issues tab should include third-party issues or not. |
| */ |
| includeCookieIssuesCausedBy: 'Include cookie Issues caused by third-party sites', |
| /** |
| * @description Label for a checkbox. Whether the issues tab should include third-party issues or not. |
| */ |
| includeThirdpartyCookieIssues: 'Include third-party cookie issues', |
| /** |
| * @description Label on the issues tab |
| */ |
| onlyThirdpartyCookieIssues: 'Only third-party cookie issues detected so far', |
| /** |
| * @description Label in the issues panel |
| */ |
| noIssuesDetectedSoFar: 'No issues detected so far', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('issues/IssuesPane.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| class IssueCategoryView extends UI.TreeOutline.TreeElement { |
| private category: SDK.Issue.IssueCategory; |
| private issues: AggregatedIssue[]; |
| |
| constructor(category: SDK.Issue.IssueCategory) { |
| super(); |
| this.category = category; |
| this.issues = []; |
| |
| this.toggleOnClick = true; |
| this.listItemElement.classList.add('issue-category'); |
| } |
| |
| getCategoryName(): string { |
| switch (this.category) { |
| case SDK.Issue.IssueCategory.CrossOriginEmbedderPolicy: |
| return i18nString(UIStrings.crossOriginEmbedderPolicy); |
| case SDK.Issue.IssueCategory.MixedContent: |
| return i18nString(UIStrings.mixedContent); |
| case SDK.Issue.IssueCategory.SameSiteCookie: |
| return i18nString(UIStrings.samesiteCookie); |
| case SDK.Issue.IssueCategory.HeavyAd: |
| return i18nString(UIStrings.heavyAds); |
| case SDK.Issue.IssueCategory.ContentSecurityPolicy: |
| return i18nString(UIStrings.contentSecurityPolicy); |
| case SDK.Issue.IssueCategory.TrustedWebActivity: |
| return i18nString(UIStrings.trustedWebActivity); |
| case SDK.Issue.IssueCategory.LowTextContrast: |
| return i18nString(UIStrings.lowTextContrast); |
| case SDK.Issue.IssueCategory.Cors: |
| return i18nString(UIStrings.cors); |
| case SDK.Issue.IssueCategory.Other: |
| return i18nString(UIStrings.other); |
| } |
| } |
| |
| onattach(): void { |
| this.appendHeader(); |
| } |
| |
| private appendHeader(): void { |
| const header = document.createElement('div'); |
| header.classList.add('header'); |
| |
| const title = document.createElement('div'); |
| title.classList.add('title'); |
| title.textContent = this.getCategoryName(); |
| header.appendChild(title); |
| |
| this.listItemElement.appendChild(header); |
| } |
| } |
| |
| export function getGroupIssuesByCategorySetting(): Common.Settings.Setting<boolean> { |
| return Common.Settings.Settings.instance().createSetting('groupIssuesByCategory', false); |
| } |
| |
| let issuesPaneInstance: IssuesPane; |
| |
| export class IssuesPane extends UI.Widget.VBox { |
| private categoryViews: Map<SDK.Issue.IssueCategory, IssueCategoryView>; |
| private issueViews: Map<string, IssueView>; |
| private showThirdPartyCheckbox: UI.Toolbar.ToolbarSettingCheckbox|null; |
| private issuesTree: UI.TreeOutline.TreeOutlineInShadow; |
| private noIssuesMessageDiv: HTMLDivElement; |
| private issuesManager: BrowserSDK.IssuesManager.IssuesManager; |
| private aggregator: IssueAggregator; |
| private issueViewUpdatePromise: Promise<void> = Promise.resolve(); |
| |
| private constructor() { |
| super(true); |
| this.registerRequiredCSS('issues/issuesPane.css', {enableLegacyPatching: false}); |
| this.contentElement.classList.add('issues-pane'); |
| |
| this.categoryViews = new Map(); |
| this.issueViews = new Map(); |
| this.showThirdPartyCheckbox = null; |
| |
| this.createToolbars(); |
| |
| this.issuesTree = new UI.TreeOutline.TreeOutlineInShadow(); |
| this.issuesTree.registerRequiredCSS('issues/issuesTree.css', {enableLegacyPatching: false}); |
| this.issuesTree.setShowSelectionOnKeyboardFocus(true); |
| this.issuesTree.contentElement.classList.add('issues'); |
| this.contentElement.appendChild(this.issuesTree.element); |
| |
| this.noIssuesMessageDiv = document.createElement('div'); |
| this.noIssuesMessageDiv.classList.add('issues-pane-no-issues'); |
| this.contentElement.appendChild(this.noIssuesMessageDiv); |
| |
| this.issuesManager = BrowserSDK.IssuesManager.IssuesManager.instance(); |
| this.aggregator = new IssueAggregator(this.issuesManager); |
| this.aggregator.addEventListener(IssueAggregatorEvents.AggregatedIssueUpdated, this.issueUpdated, this); |
| this.aggregator.addEventListener(IssueAggregatorEvents.FullUpdateRequired, this.fullUpdate, this); |
| for (const issue of this.aggregator.aggregatedIssues()) { |
| this.scheduleIssueViewUpdate(issue); |
| } |
| this.issuesManager.addEventListener(BrowserSDK.IssuesManager.Events.IssuesCountUpdated, this.updateCounts, this); |
| this.updateCounts(); |
| } |
| |
| static instance(opts: {forceNew: boolean|null} = {forceNew: null}): IssuesPane { |
| const {forceNew} = opts; |
| if (!issuesPaneInstance || forceNew) { |
| issuesPaneInstance = new IssuesPane(); |
| } |
| |
| return issuesPaneInstance; |
| } |
| |
| elementsToRestoreScrollPositionsFor(): Element[] { |
| return [this.issuesTree.element]; |
| } |
| |
| private createToolbars(): {toolbarContainer: Element} { |
| const toolbarContainer = this.contentElement.createChild('div', 'issues-toolbar-container'); |
| new UI.Toolbar.Toolbar('issues-toolbar-left', toolbarContainer); |
| const rightToolbar = new UI.Toolbar.Toolbar('issues-toolbar-right', toolbarContainer); |
| |
| const groupByCategorySetting = getGroupIssuesByCategorySetting(); |
| const groupByCategoryCheckbox = new UI.Toolbar.ToolbarSettingCheckbox( |
| groupByCategorySetting, i18nString(UIStrings.groupDisplayedIssuesUnder), i18nString(UIStrings.groupByCategory)); |
| // Hide the option to toggle category grouping for now. |
| groupByCategoryCheckbox.setVisible(false); |
| rightToolbar.appendToolbarItem(groupByCategoryCheckbox); |
| groupByCategorySetting.addChangeListener(() => { |
| this.fullUpdate(); |
| }); |
| |
| const thirdPartySetting = SDK.Issue.getShowThirdPartyIssuesSetting(); |
| this.showThirdPartyCheckbox = new UI.Toolbar.ToolbarSettingCheckbox( |
| thirdPartySetting, i18nString(UIStrings.includeCookieIssuesCausedBy), |
| i18nString(UIStrings.includeThirdpartyCookieIssues)); |
| rightToolbar.appendToolbarItem(this.showThirdPartyCheckbox); |
| this.setDefaultFocusedElement(this.showThirdPartyCheckbox.inputElement); |
| |
| rightToolbar.appendSeparator(); |
| const issueCounter = new ConsoleCounters.IssueCounter.IssueCounter(); |
| issueCounter.data = { |
| tooltipCallback: (): void => { |
| const issueEnumeration = ConsoleCounters.IssueCounter.getIssueCountsEnumeration( |
| BrowserSDK.IssuesManager.IssuesManager.instance(), false); |
| UI.Tooltip.Tooltip.install(issueCounter, issueEnumeration); |
| }, |
| displayMode: ConsoleCounters.IssueCounter.DisplayMode.ShowAlways, |
| issuesManager: BrowserSDK.IssuesManager.IssuesManager.instance(), |
| }; |
| issueCounter.id = 'console-issues-counter'; |
| const issuesToolbarItem = new UI.Toolbar.ToolbarItem(issueCounter); |
| rightToolbar.appendToolbarItem(issuesToolbarItem); |
| |
| return {toolbarContainer}; |
| } |
| |
| private issueUpdated(event: Common.EventTarget.EventTargetEvent): void { |
| const issue = event.data as AggregatedIssue; |
| this.scheduleIssueViewUpdate(issue); |
| } |
| |
| private scheduleIssueViewUpdate(issue: AggregatedIssue): void { |
| this.issueViewUpdatePromise = this.issueViewUpdatePromise.then(() => this.updateIssueView(issue)); |
| } |
| |
| /** Don't call directly. Use `scheduleIssueViewUpdate` instead. */ |
| private async updateIssueView(issue: AggregatedIssue): Promise<void> { |
| let issueView = this.issueViews.get(issue.code()); |
| if (!issueView) { |
| const description = issue.getDescription(); |
| if (!description) { |
| console.warn('Could not find description for issue code:', issue.code()); |
| return; |
| } |
| const markdownDescription = await createIssueDescriptionFromMarkdown(description); |
| issueView = new IssueView(this, issue, markdownDescription); |
| this.issueViews.set(issue.code(), issueView); |
| const parent = this.getIssueViewParent(issue); |
| parent.appendChild(issueView, (a, b) => { |
| if (a instanceof IssueView && b instanceof IssueView) { |
| return a.getIssueTitle().localeCompare(b.getIssueTitle()); |
| } |
| console.error('The issues tree should only contain IssueView objects as direct children'); |
| return 0; |
| }); |
| } |
| issueView.update(); |
| this.updateCounts(); |
| } |
| |
| private getIssueViewParent(issue: AggregatedIssue): UI.TreeOutline.TreeOutline|UI.TreeOutline.TreeElement { |
| if (!getGroupIssuesByCategorySetting().get()) { |
| return this.issuesTree; |
| } |
| |
| const category = issue.getCategory(); |
| const view = this.categoryViews.get(category); |
| if (view) { |
| return view; |
| } |
| |
| const newView = new IssueCategoryView(category); |
| this.issuesTree.appendChild(newView, (a, b) => { |
| if (a instanceof IssueCategoryView && b instanceof IssueCategoryView) { |
| return a.getCategoryName().localeCompare(b.getCategoryName()); |
| } |
| return 0; |
| }); |
| this.categoryViews.set(category, newView); |
| return newView; |
| } |
| |
| private clearViews(views: Map<unknown, UI.TreeOutline.TreeElement>): void { |
| for (const view of views.values()) { |
| view.parent && view.parent.removeChild(view); |
| } |
| views.clear(); |
| } |
| |
| private fullUpdate(): void { |
| this.clearViews(this.categoryViews); |
| this.clearViews(this.issueViews); |
| if (this.aggregator) { |
| for (const issue of this.aggregator.aggregatedIssues()) { |
| this.scheduleIssueViewUpdate(issue); |
| } |
| } |
| this.updateCounts(); |
| } |
| |
| private updateCounts(): void { |
| this.showIssuesTreeOrNoIssuesDetectedMessage(this.issuesManager.numberOfIssues()); |
| } |
| |
| private showIssuesTreeOrNoIssuesDetectedMessage(issuesCount: number): void { |
| if (issuesCount > 0) { |
| this.issuesTree.element.hidden = false; |
| this.noIssuesMessageDiv.style.display = 'none'; |
| const firstChild = this.issuesTree.firstChild(); |
| if (firstChild) { |
| firstChild.select(/* omitFocus= */ true); |
| this.setDefaultFocusedElement(firstChild.listItemElement); |
| } |
| } else { |
| this.issuesTree.element.hidden = true; |
| if (this.showThirdPartyCheckbox) { |
| this.setDefaultFocusedElement(this.showThirdPartyCheckbox.inputElement); |
| } |
| // We alreay know that issesCount is zero here. |
| const hasOnlyThirdPartyIssues = this.issuesManager.numberOfAllStoredIssues() > 0; |
| this.noIssuesMessageDiv.textContent = hasOnlyThirdPartyIssues ? i18nString(UIStrings.onlyThirdpartyCookieIssues) : |
| i18nString(UIStrings.noIssuesDetectedSoFar); |
| this.noIssuesMessageDiv.style.display = 'flex'; |
| } |
| } |
| |
| revealByCode(code: string): void { |
| const issueView = this.issueViews.get(code); |
| if (issueView) { |
| issueView.expand(); |
| issueView.reveal(); |
| } |
| } |
| } |