UNPKG

chrome-devtools-frontend

Version:
406 lines (351 loc) • 13.9 kB
// Copyright 2020 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import type * as Protocol from '../../generated/protocol.js'; import {AttributionReportingIssue} from './AttributionReportingIssue.js'; import {ContentSecurityPolicyIssue} from './ContentSecurityPolicyIssue.js'; import {CookieDeprecationMetadataIssue} from './CookieDeprecationMetadataIssue.js'; import {CookieIssue} from './CookieIssue.js'; import {CorsIssue} from './CorsIssue.js'; import {DeprecationIssue} from './DeprecationIssue.js'; import {ElementAccessibilityIssue} from './ElementAccessibilityIssue.js'; import {GenericIssue} from './GenericIssue.js'; import {HeavyAdIssue} from './HeavyAdIssue.js'; import {Issue, IssueCategory, IssueKind, unionIssueKind} from './Issue.js'; import type {EventTypes as IssuesManagerEventsTypes, IssueAddedEvent} from './IssuesManager.js'; import {Events as IssuesManagerEvents} from './IssuesManagerEvents.js'; import {LowTextContrastIssue} from './LowTextContrastIssue.js'; import type {MarkdownIssueDescription} from './MarkdownIssueDescription.js'; import {MixedContentIssue} from './MixedContentIssue.js'; import {PartitioningBlobURLIssue} from './PartitioningBlobURLIssue.js'; import {PermissionElementIssue} from './PermissionElementIssue.js'; import {QuirksModeIssue} from './QuirksModeIssue.js'; import {SharedArrayBufferIssue} from './SharedArrayBufferIssue.js'; export interface IssuesProvider extends Common.EventTarget.EventTarget<IssuesManagerEventsTypes> { issues(): Iterable<Issue>; } interface AggregationKeyTag { aggregationKeyTag: undefined; } /** * An opaque type for the key which we use to aggregate issues. The key must be * chosen such that if two aggregated issues have the same aggregation key, then * they also have the same issue code. */ export type AggregationKey = { toString(): string, }&AggregationKeyTag; /** * An `AggregatedIssue` representes a number of `IssuesManager.Issue.Issue` objects that are displayed together. * Currently only grouping by issue code, is supported. The class provides helpers to support displaying * of all resources that are affected by the aggregated issues. */ export class AggregatedIssue extends Issue { #allIssues = new Set<Issue>(); #affectedCookies = new Map<string, { cookie: Protocol.Audits.AffectedCookie, hasRequest: boolean, }>(); #affectedRawCookieLines = new Map<string, {rawCookieLine: string, hasRequest: boolean}>(); #affectedRequests: Protocol.Audits.AffectedRequest[] = []; #affectedRequestIds = new Set<Protocol.Network.RequestId>(); #affectedLocations = new Map<string, Protocol.Audits.SourceCodeLocation>(); #heavyAdIssues = new Set<HeavyAdIssue>(); #blockedByResponseDetails = new Map<string, Protocol.Audits.BlockedByResponseIssueDetails>(); #bounceTrackingSites = new Set<string>(); #corsIssues = new Set<CorsIssue>(); #cspIssues = new Set<ContentSecurityPolicyIssue>(); #deprecationIssues = new Set<DeprecationIssue>(); #issueKind = IssueKind.IMPROVEMENT; #lowContrastIssues = new Set<LowTextContrastIssue>(); #cookieDeprecationMetadataIssues = new Set<CookieDeprecationMetadataIssue>(); #mixedContentIssues = new Set<MixedContentIssue>(); #partitioningBlobURLIssues = new Set<PartitioningBlobURLIssue>(); #permissionElementIssues = new Set<PermissionElementIssue>(); #sharedArrayBufferIssues = new Set<SharedArrayBufferIssue>(); #quirksModeIssues = new Set<QuirksModeIssue>(); #attributionReportingIssues = new Set<AttributionReportingIssue>(); #genericIssues = new Set<GenericIssue>(); #elementAccessibilityIssues = new Set<ElementAccessibilityIssue>(); #representative?: Issue; #aggregatedIssuesCount = 0; #key: AggregationKey; constructor(code: string, aggregationKey: AggregationKey) { super(code, null); this.#key = aggregationKey; } override primaryKey(): string { throw new Error('This should never be called'); } aggregationKey(): AggregationKey { return this.#key; } override getBlockedByResponseDetails(): Iterable<Protocol.Audits.BlockedByResponseIssueDetails> { return this.#blockedByResponseDetails.values(); } override cookies(): Iterable<Protocol.Audits.AffectedCookie> { return Array.from(this.#affectedCookies.values()).map(x => x.cookie); } getRawCookieLines(): Iterable<{rawCookieLine: string, hasRequest: boolean}> { return this.#affectedRawCookieLines.values(); } override sources(): Iterable<Protocol.Audits.SourceCodeLocation> { return this.#affectedLocations.values(); } getBounceTrackingSites(): Iterable<string> { return this.#bounceTrackingSites.values(); } cookiesWithRequestIndicator(): Iterable<{ cookie: Protocol.Audits.AffectedCookie, hasRequest: boolean, }> { return this.#affectedCookies.values(); } getHeavyAdIssues(): Iterable<HeavyAdIssue> { return this.#heavyAdIssues; } getCookieDeprecationMetadataIssues(): Iterable<CookieDeprecationMetadataIssue> { return this.#cookieDeprecationMetadataIssues; } getMixedContentIssues(): Iterable<MixedContentIssue> { return this.#mixedContentIssues; } getCorsIssues(): Set<CorsIssue> { return this.#corsIssues; } getCspIssues(): Iterable<ContentSecurityPolicyIssue> { return this.#cspIssues; } getDeprecationIssues(): Iterable<DeprecationIssue> { return this.#deprecationIssues; } getLowContrastIssues(): Iterable<LowTextContrastIssue> { return this.#lowContrastIssues; } override requests(): Iterable<Protocol.Audits.AffectedRequest> { return this.#affectedRequests.values(); } getSharedArrayBufferIssues(): Iterable<SharedArrayBufferIssue> { return this.#sharedArrayBufferIssues; } getQuirksModeIssues(): Iterable<QuirksModeIssue> { return this.#quirksModeIssues; } getAttributionReportingIssues(): ReadonlySet<AttributionReportingIssue> { return this.#attributionReportingIssues; } getGenericIssues(): ReadonlySet<GenericIssue> { return this.#genericIssues; } getElementAccessibilityIssues(): Iterable<ElementAccessibilityIssue> { return this.#elementAccessibilityIssues; } getDescription(): MarkdownIssueDescription|null { if (this.#representative) { return this.#representative.getDescription(); } return null; } getCategory(): IssueCategory { if (this.#representative) { return this.#representative.getCategory(); } return IssueCategory.OTHER; } getAggregatedIssuesCount(): number { return this.#aggregatedIssuesCount; } getPartitioningBlobURLIssues(): Iterable<PartitioningBlobURLIssue> { return this.#partitioningBlobURLIssues; } getPermissionElementIssues(): Iterable<PermissionElementIssue> { return this.#permissionElementIssues; } /** * Produces a primary key for a cookie. Use this instead of `JSON.stringify` in * case new fields are added to `AffectedCookie`. */ #keyForCookie(cookie: Protocol.Audits.AffectedCookie): string { const {domain, path, name} = cookie; return `${domain};${path};${name}`; } addInstance(issue: Issue): void { this.#aggregatedIssuesCount++; if (!this.#representative) { this.#representative = issue; } this.#allIssues.add(issue); this.#issueKind = unionIssueKind(this.#issueKind, issue.getKind()); let hasRequest = false; for (const request of issue.requests()) { const {requestId} = request; hasRequest = true; if (requestId === undefined) { this.#affectedRequests.push(request); } else if (!this.#affectedRequestIds.has(requestId)) { this.#affectedRequests.push(request); this.#affectedRequestIds.add(requestId); } } for (const cookie of issue.cookies()) { const key = this.#keyForCookie(cookie); if (!this.#affectedCookies.has(key)) { this.#affectedCookies.set(key, {cookie, hasRequest}); } } for (const rawCookieLine of issue.rawCookieLines()) { if (!this.#affectedRawCookieLines.has(rawCookieLine)) { this.#affectedRawCookieLines.set(rawCookieLine, {rawCookieLine, hasRequest}); } } for (const site of issue.trackingSites()) { if (!this.#bounceTrackingSites.has(site)) { this.#bounceTrackingSites.add(site); } } for (const location of issue.sources()) { const key = JSON.stringify(location); if (!this.#affectedLocations.has(key)) { this.#affectedLocations.set(key, location); } } if (issue instanceof CookieDeprecationMetadataIssue) { this.#cookieDeprecationMetadataIssues.add(issue); } if (issue instanceof MixedContentIssue) { this.#mixedContentIssues.add(issue); } if (issue instanceof HeavyAdIssue) { this.#heavyAdIssues.add(issue); } for (const details of issue.getBlockedByResponseDetails()) { const key = JSON.stringify(details, ['parentFrame', 'blockedFrame', 'requestId', 'frameId', 'reason', 'request']); this.#blockedByResponseDetails.set(key, details); } if (issue instanceof ContentSecurityPolicyIssue) { this.#cspIssues.add(issue); } if (issue instanceof DeprecationIssue) { this.#deprecationIssues.add(issue); } if (issue instanceof SharedArrayBufferIssue) { this.#sharedArrayBufferIssues.add(issue); } if (issue instanceof LowTextContrastIssue) { this.#lowContrastIssues.add(issue); } if (issue instanceof CorsIssue) { this.#corsIssues.add(issue); } if (issue instanceof QuirksModeIssue) { this.#quirksModeIssues.add(issue); } if (issue instanceof AttributionReportingIssue) { this.#attributionReportingIssues.add(issue); } if (issue instanceof GenericIssue) { this.#genericIssues.add(issue); } if (issue instanceof ElementAccessibilityIssue) { this.#elementAccessibilityIssues.add(issue); } if (issue instanceof PartitioningBlobURLIssue) { this.#partitioningBlobURLIssues.add(issue); } if (issue instanceof PermissionElementIssue) { this.#permissionElementIssues.add(issue); } } getKind(): IssueKind { return this.#issueKind; } getAllIssues(): Issue[] { return Array.from(this.#allIssues); } override isHidden(): boolean { return this.#representative?.isHidden() || false; } override setHidden(_value: boolean): void { throw new Error('Should not call setHidden on aggregatedIssue'); } } export class IssueAggregator extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { readonly #aggregatedIssuesByKey = new Map<AggregationKey, AggregatedIssue>(); readonly #hiddenAggregatedIssuesByKey = new Map<AggregationKey, AggregatedIssue>(); constructor(private readonly issuesManager: IssuesProvider) { super(); this.issuesManager.addEventListener(IssuesManagerEvents.ISSUE_ADDED, this.#onIssueAdded, this); this.issuesManager.addEventListener(IssuesManagerEvents.FULL_UPDATE_REQUIRED, this.#onFullUpdateRequired, this); for (const issue of this.issuesManager.issues()) { this.#aggregateIssue(issue); } } #onIssueAdded(event: Common.EventTarget.EventTargetEvent<IssueAddedEvent>): void { this.#aggregateIssue(event.data.issue); } #onFullUpdateRequired(): void { this.#aggregatedIssuesByKey.clear(); this.#hiddenAggregatedIssuesByKey.clear(); for (const issue of this.issuesManager.issues()) { this.#aggregateIssue(issue); } this.dispatchEventToListeners(Events.FULL_UPDATE_REQUIRED); } #aggregateIssue(issue: Issue): AggregatedIssue|undefined { if (CookieIssue.isThirdPartyCookiePhaseoutRelatedIssue(issue)) { return; } const map = issue.isHidden() ? this.#hiddenAggregatedIssuesByKey : this.#aggregatedIssuesByKey; const aggregatedIssue = this.#aggregateIssueByStatus(map, issue); this.dispatchEventToListeners(Events.AGGREGATED_ISSUE_UPDATED, aggregatedIssue); return aggregatedIssue; } #aggregateIssueByStatus(aggregatedIssuesMap: Map<AggregationKey, AggregatedIssue>, issue: Issue): AggregatedIssue { const key = issue.code() as unknown as AggregationKey; let aggregatedIssue = aggregatedIssuesMap.get(key); if (!aggregatedIssue) { aggregatedIssue = new AggregatedIssue(issue.code(), key); aggregatedIssuesMap.set(key, aggregatedIssue); } aggregatedIssue.addInstance(issue); return aggregatedIssue; } aggregatedIssues(): Iterable<AggregatedIssue> { return [...this.#aggregatedIssuesByKey.values(), ...this.#hiddenAggregatedIssuesByKey.values()]; } aggregatedIssueCodes(): Set<AggregationKey> { return new Set([...this.#aggregatedIssuesByKey.keys(), ...this.#hiddenAggregatedIssuesByKey.keys()]); } aggregatedIssueCategories(): Set<IssueCategory> { const result = new Set<IssueCategory>(); for (const issue of this.#aggregatedIssuesByKey.values()) { result.add(issue.getCategory()); } return result; } aggregatedIssueKinds(): Set<IssueKind> { const result = new Set<IssueKind>(); for (const issue of this.#aggregatedIssuesByKey.values()) { result.add(issue.getKind()); } return result; } numberOfAggregatedIssues(): number { return this.#aggregatedIssuesByKey.size; } numberOfHiddenAggregatedIssues(): number { return this.#hiddenAggregatedIssuesByKey.size; } keyForIssue(issue: Issue): AggregationKey { return issue.code() as unknown as AggregationKey; } } export const enum Events { AGGREGATED_ISSUE_UPDATED = 'AggregatedIssueUpdated', FULL_UPDATE_REQUIRED = 'FullUpdateRequired', } export interface EventTypes { [Events.AGGREGATED_ISSUE_UPDATED]: AggregatedIssue; [Events.FULL_UPDATE_REQUIRED]: void; }