chrome-devtools-frontend
Version:
Chrome DevTools UI
629 lines (549 loc) • 23.4 kB
text/typescript
// Copyright 2014 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 * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type {UISourceCode} from './UISourceCode.js';
import {projectTypes} from './WorkspaceImpl.js';
const UIStrings = {
/**
* @description Text to stop preventing the debugger from stepping into library code
*/
removeFromIgnoreList: 'Remove from ignore list',
/**
* @description Text for scripts that should not be stepped into when debugging
*/
addScriptToIgnoreList: 'Add script to ignore list',
/**
* @description Text for directories whose scripts should not be stepped into when debugging
*/
addDirectoryToIgnoreList: 'Add directory to ignore list',
/**
* @description A context menu item in the Call Stack Sidebar Pane of the Sources panel
*/
addAllContentScriptsToIgnoreList: 'Add all extension scripts to ignore list',
/**
* @description A context menu item in the Call Stack Sidebar Pane of the Sources panel
*/
addAllThirdPartyScriptsToIgnoreList: 'Add all third-party scripts to ignore list',
/**
* @description A context menu item in the Call Stack Sidebar Pane of the Sources panel
*/
addAllAnonymousScriptsToIgnoreList: 'Add all anonymous scripts to ignore list',
} as const;
const str_ = i18n.i18n.registerUIStrings('models/workspace/IgnoreListManager.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let ignoreListManagerInstance: IgnoreListManager|undefined;
export interface IgnoreListGeneralRules {
isContentScript?: boolean;
isKnownThirdParty?: boolean;
isCurrentlyIgnoreListed?: boolean;
}
export class IgnoreListManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements
SDK.TargetManager.SDKModelObserver<SDK.DebuggerModel.DebuggerModel> {
readonly #listeners: Set<() => void>;
readonly #isIgnoreListedURLCache: Map<string, boolean>;
readonly #contentScriptExecutionContexts: Set<string>;
private constructor() {
super();
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.GlobalObjectCleared,
this.clearCacheIfNeeded.bind(this), this);
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.RuntimeModel.RuntimeModel, SDK.RuntimeModel.Events.ExecutionContextCreated, this.onExecutionContextCreated,
this, {scoped: true});
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.RuntimeModel.RuntimeModel, SDK.RuntimeModel.Events.ExecutionContextDestroyed,
this.onExecutionContextDestroyed, this, {scoped: true});
Common.Settings.Settings.instance()
.moduleSetting('skip-stack-frames-pattern')
.addChangeListener(this.patternChanged.bind(this));
Common.Settings.Settings.instance()
.moduleSetting('skip-content-scripts')
.addChangeListener(this.patternChanged.bind(this));
Common.Settings.Settings.instance()
.moduleSetting('automatically-ignore-list-known-third-party-scripts')
.addChangeListener(this.patternChanged.bind(this));
Common.Settings.Settings.instance()
.moduleSetting('enable-ignore-listing')
.addChangeListener(this.patternChanged.bind(this));
Common.Settings.Settings.instance()
.moduleSetting('skip-anonymous-scripts')
.addChangeListener(this.patternChanged.bind(this));
this.#listeners = new Set();
this.#isIgnoreListedURLCache = new Map();
this.#contentScriptExecutionContexts = new Set();
SDK.TargetManager.TargetManager.instance().observeModels(SDK.DebuggerModel.DebuggerModel, this);
}
static instance(opts: {
forceNew: boolean|null,
} = {forceNew: null}): IgnoreListManager {
const {forceNew} = opts;
if (!ignoreListManagerInstance || forceNew) {
ignoreListManagerInstance = new IgnoreListManager();
}
return ignoreListManagerInstance;
}
static removeInstance(): void {
ignoreListManagerInstance = undefined;
}
addChangeListener(listener: () => void): void {
this.#listeners.add(listener);
}
removeChangeListener(listener: () => void): void {
this.#listeners.delete(listener);
}
modelAdded(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
void this.setIgnoreListPatterns(debuggerModel);
const sourceMapManager = debuggerModel.sourceMapManager();
sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapAttached, this.sourceMapAttached, this);
sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapDetached, this.sourceMapDetached, this);
}
modelRemoved(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
this.clearCacheIfNeeded();
const sourceMapManager = debuggerModel.sourceMapManager();
sourceMapManager.removeEventListener(SDK.SourceMapManager.Events.SourceMapAttached, this.sourceMapAttached, this);
sourceMapManager.removeEventListener(SDK.SourceMapManager.Events.SourceMapDetached, this.sourceMapDetached, this);
}
private isContentScript(executionContext: SDK.RuntimeModel.ExecutionContext): boolean {
return !executionContext.isDefault;
}
private onExecutionContextCreated(event: Common.EventTarget.EventTargetEvent<SDK.RuntimeModel.ExecutionContext>):
void {
if (this.isContentScript(event.data)) {
this.#contentScriptExecutionContexts.add(event.data.uniqueId);
if (this.skipContentScripts) {
for (const debuggerModel of SDK.TargetManager.TargetManager.instance().models(
SDK.DebuggerModel.DebuggerModel)) {
void this.updateIgnoredExecutionContexts(debuggerModel);
}
}
}
}
private onExecutionContextDestroyed(event: Common.EventTarget.EventTargetEvent<SDK.RuntimeModel.ExecutionContext>):
void {
if (this.isContentScript(event.data)) {
this.#contentScriptExecutionContexts.delete(event.data.uniqueId);
if (this.skipContentScripts) {
for (const debuggerModel of SDK.TargetManager.TargetManager.instance().models(
SDK.DebuggerModel.DebuggerModel)) {
void this.updateIgnoredExecutionContexts(debuggerModel);
}
}
}
}
private clearCacheIfNeeded(): void {
if (this.#isIgnoreListedURLCache.size > 1024) {
this.#isIgnoreListedURLCache.clear();
}
}
private getSkipStackFramesPatternSetting(): Common.Settings.RegExpSetting {
return Common.Settings.Settings.instance().moduleSetting('skip-stack-frames-pattern') as
Common.Settings.RegExpSetting;
}
private setIgnoreListPatterns(debuggerModel: SDK.DebuggerModel.DebuggerModel): Promise<boolean> {
const regexPatterns = this.enableIgnoreListing ? this.getSkipStackFramesPatternSetting().getAsArray() : [];
const patterns = ([] as string[]);
for (const item of regexPatterns) {
if (!item.disabled && item.pattern) {
patterns.push(item.pattern);
}
}
return debuggerModel.setBlackboxPatterns(patterns, this.skipAnonymousScripts);
}
private updateIgnoredExecutionContexts(debuggerModel: SDK.DebuggerModel.DebuggerModel): Promise<boolean> {
return debuggerModel.setBlackboxExecutionContexts(
this.skipContentScripts ? Array.from(this.#contentScriptExecutionContexts) : []);
}
private getGeneralRulesForUISourceCode(uiSourceCode: UISourceCode): IgnoreListGeneralRules {
const projectType = uiSourceCode.project().type();
const isContentScript = projectType === projectTypes.ContentScripts;
const isKnownThirdParty = uiSourceCode.isKnownThirdParty();
return {isContentScript, isKnownThirdParty};
}
isUserOrSourceMapIgnoreListedUISourceCode(uiSourceCode: UISourceCode): boolean {
if (uiSourceCode.isUnconditionallyIgnoreListed()) {
return true;
}
const url = this.uiSourceCodeURL(uiSourceCode);
return this.isUserIgnoreListedURL(url, this.getGeneralRulesForUISourceCode(uiSourceCode));
}
isUserIgnoreListedURL(url: Platform.DevToolsPath.UrlString|null, options?: IgnoreListGeneralRules): boolean {
if (!this.enableIgnoreListing) {
return false;
}
if (options?.isContentScript && this.skipContentScripts) {
return true;
}
if (options?.isKnownThirdParty && this.automaticallyIgnoreListKnownThirdPartyScripts) {
return true;
}
if (!url) {
return this.skipAnonymousScripts;
}
if (this.#isIgnoreListedURLCache.has(url)) {
return Boolean(this.#isIgnoreListedURLCache.get(url));
}
const isIgnoreListed = this.getFirstMatchedRegex(url) !== null;
this.#isIgnoreListedURLCache.set(url, isIgnoreListed);
return isIgnoreListed;
}
getFirstMatchedRegex(url: Platform.DevToolsPath.UrlString): RegExp|null {
if (!url) {
return null;
}
const regexPatterns = this.getSkipStackFramesPatternSetting().getAsArray();
const regexValue = this.urlToRegExpString(url);
if (!regexValue) {
return null;
}
for (let i = 0; i < regexPatterns.length; ++i) {
const item = regexPatterns[i];
if (item.disabled || item.disabledForUrl === url) {
continue;
}
const regex = new RegExp(item.pattern);
if (regex.test(url)) {
return regex;
}
}
return null;
}
private sourceMapAttached(
event: Common.EventTarget.EventTargetEvent<{client: SDK.Script.Script, sourceMap: SDK.SourceMap.SourceMap}>):
void {
const script = event.data.client;
const sourceMap = event.data.sourceMap;
void this.updateScriptRanges(script, sourceMap);
}
private sourceMapDetached(
event: Common.EventTarget.EventTargetEvent<{client: SDK.Script.Script, sourceMap: SDK.SourceMap.SourceMap}>):
void {
const script = event.data.client;
void this.updateScriptRanges(script, undefined);
}
private async updateScriptRanges(script: SDK.Script.Script, sourceMap: SDK.SourceMap.SourceMap|undefined):
Promise<void> {
let hasIgnoreListedMappings = false;
if (!IgnoreListManager.instance().isUserIgnoreListedURL(
script.sourceURL, {isContentScript: script.isContentScript()})) {
hasIgnoreListedMappings =
sourceMap?.sourceURLs().some(
url => this.isUserIgnoreListedURL(url, {isKnownThirdParty: sourceMap.hasIgnoreListHint(url)})) ??
false;
}
if (!hasIgnoreListedMappings) {
if (scriptToRange.get(script) && await script.setBlackboxedRanges([])) {
scriptToRange.delete(script);
}
this.dispatchEventToListeners(Events.IGNORED_SCRIPT_RANGES_UPDATED, script);
return;
}
if (!sourceMap) {
return;
}
const newRanges =
sourceMap
.findRanges(
srcURL => this.isUserIgnoreListedURL(srcURL, {isKnownThirdParty: sourceMap.hasIgnoreListHint(srcURL)}),
{isStartMatching: true})
.flatMap(range => [range.start, range.end]);
const oldRanges = scriptToRange.get(script) || [];
if (!isEqual(oldRanges, newRanges) && await script.setBlackboxedRanges(newRanges)) {
scriptToRange.set(script, newRanges);
}
this.dispatchEventToListeners(Events.IGNORED_SCRIPT_RANGES_UPDATED, script);
function isEqual(rangesA: SourceRange[], rangesB: SourceRange[]): boolean {
if (rangesA.length !== rangesB.length) {
return false;
}
for (let i = 0; i < rangesA.length; ++i) {
if (rangesA[i].lineNumber !== rangesB[i].lineNumber || rangesA[i].columnNumber !== rangesB[i].columnNumber) {
return false;
}
}
return true;
}
}
private uiSourceCodeURL(uiSourceCode: UISourceCode): Platform.DevToolsPath.UrlString|null {
return uiSourceCode.project().type() === projectTypes.Debugger ? null : uiSourceCode.url();
}
canIgnoreListUISourceCode(uiSourceCode: UISourceCode): boolean {
const url = this.uiSourceCodeURL(uiSourceCode);
return url ? Boolean(this.urlToRegExpString(url)) : false;
}
ignoreListUISourceCode(uiSourceCode: UISourceCode): void {
const url = this.uiSourceCodeURL(uiSourceCode);
if (url) {
this.ignoreListURL(url);
}
}
unIgnoreListUISourceCode(uiSourceCode: UISourceCode): void {
this.unIgnoreListURL(this.uiSourceCodeURL(uiSourceCode), this.getGeneralRulesForUISourceCode(uiSourceCode));
}
get enableIgnoreListing(): boolean {
return Common.Settings.Settings.instance().moduleSetting('enable-ignore-listing').get();
}
set enableIgnoreListing(value: boolean) {
Common.Settings.Settings.instance().moduleSetting('enable-ignore-listing').set(value);
}
get skipContentScripts(): boolean {
return this.enableIgnoreListing && Common.Settings.Settings.instance().moduleSetting('skip-content-scripts').get();
}
get skipAnonymousScripts(): boolean {
return this.enableIgnoreListing &&
Common.Settings.Settings.instance().moduleSetting('skip-anonymous-scripts').get();
}
get automaticallyIgnoreListKnownThirdPartyScripts(): boolean {
return this.enableIgnoreListing &&
Common.Settings.Settings.instance().moduleSetting('automatically-ignore-list-known-third-party-scripts').get();
}
ignoreListContentScripts(): void {
if (!this.enableIgnoreListing) {
this.enableIgnoreListing = true;
}
Common.Settings.Settings.instance().moduleSetting('skip-content-scripts').set(true);
}
unIgnoreListContentScripts(): void {
Common.Settings.Settings.instance().moduleSetting('skip-content-scripts').set(false);
}
ignoreListAnonymousScripts(): void {
if (!this.enableIgnoreListing) {
this.enableIgnoreListing = true;
}
Common.Settings.Settings.instance().moduleSetting('skip-anonymous-scripts').set(true);
}
unIgnoreListAnonymousScripts(): void {
Common.Settings.Settings.instance().moduleSetting('skip-anonymous-scripts').set(false);
}
ignoreListThirdParty(): void {
if (!this.enableIgnoreListing) {
this.enableIgnoreListing = true;
}
Common.Settings.Settings.instance().moduleSetting('automatically-ignore-list-known-third-party-scripts').set(true);
}
unIgnoreListThirdParty(): void {
Common.Settings.Settings.instance().moduleSetting('automatically-ignore-list-known-third-party-scripts').set(false);
}
ignoreListURL(url: Platform.DevToolsPath.UrlString): void {
const regexValue = this.urlToRegExpString(url);
if (!regexValue) {
return;
}
this.addRegexToIgnoreList(regexValue, url);
}
addRegexToIgnoreList(regexValue: string, disabledForUrl?: Platform.DevToolsPath.UrlString): void {
const regexPatterns = this.getSkipStackFramesPatternSetting().getAsArray();
let found = false;
for (let i = 0; i < regexPatterns.length; ++i) {
const item = regexPatterns[i];
if (item.pattern === regexValue || (disabledForUrl && item.disabledForUrl === disabledForUrl)) {
item.disabled = false;
item.disabledForUrl = undefined;
found = true;
}
}
if (!found) {
regexPatterns.push({pattern: regexValue, disabled: false});
}
if (!this.enableIgnoreListing) {
this.enableIgnoreListing = true;
}
this.getSkipStackFramesPatternSetting().setAsArray(regexPatterns);
}
unIgnoreListURL(url: Platform.DevToolsPath.UrlString|null, options?: IgnoreListGeneralRules): void {
if (options?.isContentScript) {
this.unIgnoreListContentScripts();
}
if (options?.isKnownThirdParty) {
this.unIgnoreListThirdParty();
}
if (!url) {
this.unIgnoreListAnonymousScripts();
return;
}
let regexPatterns = this.getSkipStackFramesPatternSetting().getAsArray();
const regexValue = IgnoreListManager.instance().urlToRegExpString(url);
if (!regexValue) {
return;
}
regexPatterns = regexPatterns.filter(function(item) {
return item.pattern !== regexValue;
});
for (let i = 0; i < regexPatterns.length; ++i) {
const item = regexPatterns[i];
if (item.disabled) {
continue;
}
try {
const regex = new RegExp(item.pattern);
if (regex.test(url)) {
item.disabled = true;
item.disabledForUrl = url;
}
} catch {
}
}
this.getSkipStackFramesPatternSetting().setAsArray(regexPatterns);
}
private removeIgnoreListPattern(regexValue: string): void {
let regexPatterns = this.getSkipStackFramesPatternSetting().getAsArray();
regexPatterns = regexPatterns.filter(function(item) {
return item.pattern !== regexValue;
});
this.getSkipStackFramesPatternSetting().setAsArray(regexPatterns);
}
private ignoreListHasPattern(regexValue: string, enabledOnly: boolean): boolean {
const regexPatterns = this.getSkipStackFramesPatternSetting().getAsArray();
return regexPatterns.some(item => !(enabledOnly && item.disabled) && item.pattern === regexValue);
}
private async patternChanged(): Promise<void> {
this.#isIgnoreListedURLCache.clear();
const promises: Array<Promise<unknown>> = [];
for (const debuggerModel of SDK.TargetManager.TargetManager.instance().models(SDK.DebuggerModel.DebuggerModel)) {
promises.push(this.setIgnoreListPatterns(debuggerModel));
const sourceMapManager = debuggerModel.sourceMapManager();
for (const script of debuggerModel.scripts()) {
promises.push(this.updateScriptRanges(script, sourceMapManager.sourceMapForClient(script)));
}
promises.push(this.updateIgnoredExecutionContexts(debuggerModel));
}
await Promise.all(promises);
const listeners = Array.from(this.#listeners);
for (const listener of listeners) {
listener();
}
this.patternChangeFinishedForTests();
}
private patternChangeFinishedForTests(): void {
// This method is sniffed in tests.
}
private urlToRegExpString(url: Platform.DevToolsPath.UrlString): string {
const parsedURL = new Common.ParsedURL.ParsedURL(url);
if (parsedURL.isAboutBlank() || parsedURL.isDataURL()) {
return '';
}
if (!parsedURL.isValid) {
return '^' + Platform.StringUtilities.escapeForRegExp(url) + '$';
}
let name: string = parsedURL.lastPathComponent;
if (name) {
name = '/' + name;
} else if (parsedURL.folderPathComponents) {
name = parsedURL.folderPathComponents + '/';
}
if (!name) {
name = parsedURL.host;
}
if (!name) {
return '';
}
const scheme = parsedURL.scheme;
let prefix = '';
if (scheme && scheme !== 'http' && scheme !== 'https') {
prefix = '^' + scheme + '://';
if (scheme === 'chrome-extension') {
prefix += parsedURL.host + '\\b';
}
prefix += '.*';
}
return prefix + Platform.StringUtilities.escapeForRegExp(name) + (url.endsWith(name) ? '$' : '\\b');
}
getIgnoreListURLContextMenuItems(uiSourceCode: UISourceCode):
Array<{text: string, callback: () => void, jslogContext: string}> {
if (uiSourceCode.project().type() === projectTypes.FileSystem) {
return [];
}
const menuItems: Array<{text: string, callback: () => void, jslogContext: string}> = [];
const canIgnoreList = this.canIgnoreListUISourceCode(uiSourceCode);
const isIgnoreListed = this.isUserOrSourceMapIgnoreListedUISourceCode(uiSourceCode);
const isAnonymous = !this.uiSourceCodeURL(uiSourceCode);
const {isContentScript, isKnownThirdParty} = this.getGeneralRulesForUISourceCode(uiSourceCode);
if (isIgnoreListed) {
if (canIgnoreList || isContentScript || isKnownThirdParty || isAnonymous) {
menuItems.push({
text: i18nString(UIStrings.removeFromIgnoreList),
callback: this.unIgnoreListUISourceCode.bind(this, uiSourceCode),
jslogContext: 'remove-script-from-ignorelist',
});
}
} else {
if (canIgnoreList) {
menuItems.push({
text: i18nString(UIStrings.addScriptToIgnoreList),
callback: this.ignoreListUISourceCode.bind(this, uiSourceCode),
jslogContext: 'add-script-to-ignorelist',
});
} else if (isAnonymous) {
menuItems.push({
text: i18nString(UIStrings.addAllAnonymousScriptsToIgnoreList),
callback: this.ignoreListAnonymousScripts.bind(this),
jslogContext: 'add-anonymous-scripts-to-ignorelist',
});
}
menuItems.push(...this.getIgnoreListGeneralContextMenuItems({isContentScript, isKnownThirdParty}));
}
return menuItems;
}
private getIgnoreListGeneralContextMenuItems(options?: IgnoreListGeneralRules):
Array<{text: string, callback: () => void, jslogContext: string}> {
const menuItems: Array<{text: string, callback: () => void, jslogContext: string}> = [];
if (options?.isContentScript) {
menuItems.push({
text: i18nString(UIStrings.addAllContentScriptsToIgnoreList),
callback: this.ignoreListContentScripts.bind(this),
jslogContext: 'add-content-scripts-to-ignorelist',
});
}
if (options?.isKnownThirdParty) {
menuItems.push({
text: i18nString(UIStrings.addAllThirdPartyScriptsToIgnoreList),
callback: this.ignoreListThirdParty.bind(this),
jslogContext: 'add-3p-scripts-to-ignorelist',
});
}
return menuItems;
}
getIgnoreListFolderContextMenuItems(url: Platform.DevToolsPath.UrlString, options?: IgnoreListGeneralRules):
Array<{text: string, callback: () => void, jslogContext: string}> {
const menuItems: Array<{text: string, callback: () => void, jslogContext: string}> = [];
const regexValue = '^' + Platform.StringUtilities.escapeForRegExp(url) + '/';
if (this.ignoreListHasPattern(regexValue, true)) {
menuItems.push({
text: i18nString(UIStrings.removeFromIgnoreList),
callback: this.removeIgnoreListPattern.bind(this, regexValue),
jslogContext: 'remove-from-ignore-list',
});
} else if (this.isUserIgnoreListedURL(url, options)) {
// This specific url isn't on the ignore list, but there are rules that match it.
menuItems.push({
text: i18nString(UIStrings.removeFromIgnoreList),
callback: this.unIgnoreListURL.bind(this, url, options),
jslogContext: 'remove-from-ignore-list',
});
} else if (!options?.isCurrentlyIgnoreListed) {
// Provide options to add to ignore list, unless folder currently displays
// as entirely ignored.
menuItems.push({
text: i18nString(UIStrings.addDirectoryToIgnoreList),
callback: this.addRegexToIgnoreList.bind(this, regexValue),
jslogContext: 'add-directory-to-ignore-list',
});
menuItems.push(...this.getIgnoreListGeneralContextMenuItems(options));
}
return menuItems;
}
}
export interface SourceRange {
lineNumber: number;
columnNumber: number;
}
const scriptToRange = new WeakMap<SDK.Script.Script, SourceRange[]>();
export const enum Events {
IGNORED_SCRIPT_RANGES_UPDATED = 'IGNORED_SCRIPT_RANGES_UPDATED',
}
export interface EventTypes {
[Events.IGNORED_SCRIPT_RANGES_UPDATED]: SDK.Script.Script;
}