Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions extension/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,33 @@ function parseDefaultsFlag(argv: readonly string[]): string | undefined {
return undefined;
}

// Parse a dotenv-style file body for `name`, returning the (de-quoted) value
// or undefined if the key isn't present.
function findEnvValueInContent(
content: string,
name: string,
): string | undefined {
for (const rawLine of content.split("\n")) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) {
continue;
}
const eq = line.indexOf("=");
if (eq === -1 || line.slice(0, eq).trim() !== name) {
continue;
}
let value = line.slice(eq + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
return value;
}
return undefined;
}

function readEnvValue(name: string): string {
if (process.env[name]) {
return process.env[name] ?? "";
Expand All @@ -49,25 +76,8 @@ function readEnvValue(name: string): string {
} catch {
continue;
}
for (const rawLine of content.split("\n")) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) {
continue;
}
const eq = line.indexOf("=");
if (eq === -1) {
continue;
}
if (line.slice(0, eq).trim() !== name) {
continue;
}
let value = line.slice(eq + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
const value = findEnvValueInContent(content, name);
if (value !== undefined) {
return value;
}
}
Expand Down
11 changes: 9 additions & 2 deletions extension/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,15 @@ export default tseslint.config(
// so enabling it means breaking shadow-DOM scans or scoped-disabling
// ~10+ correct sites. Not worth it. See #279.
"unicorn/prefer-scoped-selector": "off",
// Warn — ratchet to error in #279:
"unicorn/no-break-in-nested-loop": "warn",
// Off — not a clean ratchet (see #279). The rule treats a `switch`
// inside a loop as a "nested loop", so ~9 sites are idiomatic
// switch-case `break`s (required syntax, not loop control) that it
// would force into pointless function extractions. The genuinely
// improvable production sites — guard inversions, `.some()`, and small
// helper extractions — were cleaned up directly, but the rule itself
// can't be narrowed to exclude `switch` or the remaining test/mock
// sites, so leaving it on would be all churn for no further upside.
"unicorn/no-break-in-nested-loop": "off",
// no-global-object-property-assignment stays at its recommended `error`
// for production code; it's disabled only for tests (which legitimately
// assign globals to set up mocks) in the test-files block below.
Expand Down
43 changes: 26 additions & 17 deletions extension/src/lib/rule-count.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,29 +114,38 @@ function currentCounts(): Record<string, number> {
if (matches.length > 0) {
counts[ruleId] = (counts[ruleId] ?? 0) + matches.length;
}
if (!traceCssMatches) {
continue;
}
for (const element of matches) {
if (reg.traced.has(element)) {
continue;
}
reg.traced.add(element);
const html = element.outerHTML;
recordRuleApplication({
ruleId,
kind: "hide",
selector: reg.union,
beforeHtml: html,
afterHtml: html,
cssOnly: true,
});
if (traceCssMatches) {
traceUnionMatches(ruleId, reg, matches);
}
}
}
return counts;
}

// Emit a debug-trace `rule-application` event per newly-matched element for a
// CSS-first union. See currentCounts() for why these are `cssOnly`.
function traceUnionMatches(
ruleId: string,
reg: CssFirstUnion,
matches: NodeListOf<Element>,
): void {
for (const element of matches) {
if (reg.traced.has(element)) {
continue;
}
reg.traced.add(element);
const html = element.outerHTML;
recordRuleApplication({
ruleId,
kind: "hide",
selector: reg.union,
beforeHtml: html,
afterHtml: html,
cssOnly: true,
});
}
}

function shallowEqual(
a: Record<string, number>,
b: Record<string, number>,
Expand Down
7 changes: 1 addition & 6 deletions extension/src/lib/selector-token-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,7 @@ function dispatchToRules(roots: Element[]): void {
? new Set(registrations.keys())
: findTriggeredRules(root);
for (const ruleId of triggered) {
const registration = registrations.get(ruleId);

if (!registration) {
continue;
}
registration.dispatchScan(root);
registrations.get(ruleId)?.dispatchScan(root);
}
}
}
Expand Down
63 changes: 34 additions & 29 deletions extension/src/lib/subtree-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,41 @@ function enqueueAttributeMutation(
}
}

// Dispatch a single added node into the router. The shared filters
// (nodeType, IGNORE_TAGS, isConnected) run once per node regardless of
// subscriber count — the whole point of the router.
function dispatchAddedNode(router: Router, added: Node): void {
if (added.nodeType !== Node.ELEMENT_NODE) {
return;
}
const element = added as Element;
if (IGNORE_TAGS.has(element.tagName)) {
return;
}
// React-style reconciliation routinely adds-then-removes a node in
// the same tick. By the time MutationObserver fires (microtask
// afterward), the addition record is still there but the node is
// already detached. drainSubscriber() filters by isConnected too —
// checking at enqueue avoids buffering churn in pending during a
// 5k-node route swap with high in-tick removal rate.
if (!element.isConnected) {
return;
}
enqueueForAllSubscribers(router, element);
// A freshly-inserted host element may already have a populated
// open shadow root (custom elements that build their shadow in
// the constructor, lit/stencil components, etc.). adoptShadowRoot
// observes the root and dispatches its initial children — without
// this, mutation observation would only catch FUTURE additions
// into the shadow, never the content present at insertion time.
for (const shadow of discoverShadowRootsIn(element)) {
adoptShadowRoot(router, shadow);
}
}

function fanOut(router: Router, mutations: MutationRecord[]): void {
// Walk every added node once and dispatch into each subscriber's pending
// set. The shared filters (nodeType, IGNORE_TAGS, isConnected) run once
// per node regardless of subscriber count — the whole point of the
// router. Per-subscriber filters (skipPlaceholderSubtrees) still run
// set. Per-subscriber filters (skipPlaceholderSubtrees) still run
// per (node, subscriber) pair, but they're cheap classlist reads.
for (const mutation of mutations) {
if (mutation.type === "attributes") {
Expand All @@ -208,32 +238,7 @@ function fanOut(router: Router, mutations: MutationRecord[]): void {
continue;
}
for (const added of mutation.addedNodes) {
if (added.nodeType !== Node.ELEMENT_NODE) {
continue;
}
const element = added as Element;
if (IGNORE_TAGS.has(element.tagName)) {
continue;
}
// React-style reconciliation routinely adds-then-removes a node in
// the same tick. By the time MutationObserver fires (microtask
// afterward), the addition record is still there but the node is
// already detached. drainSubscriber() filters by isConnected too —
// checking at enqueue avoids buffering churn in pending during a
// 5k-node route swap with high in-tick removal rate.
if (!element.isConnected) {
continue;
}
enqueueForAllSubscribers(router, element);
// A freshly-inserted host element may already have a populated
// open shadow root (custom elements that build their shadow in
// the constructor, lit/stencil components, etc.). adoptShadowRoot
// observes the root and dispatches its initial children — without
// this, mutation observation would only catch FUTURE additions
// into the shadow, never the content present at insertion time.
for (const shadow of discoverShadowRootsIn(element)) {
adoptShadowRoot(router, shadow);
}
dispatchAddedNode(router, added);
}
}

Expand Down
5 changes: 2 additions & 3 deletions extension/src/rules/countdown-timer-redact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,9 @@ const watcher = createSubtreeWatcher({
const candidates: Candidate[] = [];
for (const root of roots) {
for (const candidate of findCandidates(root)) {
if (trackedElements.has(candidate.element)) {
continue;
if (!trackedElements.has(candidate.element)) {
candidates.push(candidate);
}
candidates.push(candidate);
}
}
scheduleReconcile(candidates);
Expand Down
10 changes: 3 additions & 7 deletions extension/src/rules/disguised-ad-flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,13 +300,9 @@ function isArticleShaped(element: Element, labelElement: Element): boolean {
if (child.querySelector(HEADING_SELECTOR) !== null) {
continue;
}
let hasNestedCandidate = false;
for (const nested of child.querySelectorAll("p, span, div, li")) {
if (candidateSet.has(nested)) {
hasNestedCandidate = true;
break;
}
}
const hasNestedCandidate = [
...child.querySelectorAll("p, span, div, li"),
].some((nested) => candidateSet.has(nested));
if (hasNestedCandidate) {
continue;
}
Expand Down
21 changes: 10 additions & 11 deletions extension/src/rules/encoded-payload-redact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,21 +721,20 @@ function collectSubstitutionCiphers(
) {
continue;
}
for (const decoder of SUBSTITUTION_DECODERS) {
if (
const decodes = SUBSTITUTION_DECODERS.some(
(decoder) =>
tryCipherDecode(
candidate,
decoder,
SUB_RULES.substitutionCipher.minCommonWords,
) !== null
) {
matches.push({
start: m.index,
end: m.index + candidate.length,
label: "[encoded payload hidden]",
});
break;
}
) !== null,
);
if (decodes) {
matches.push({
start: m.index,
end: m.index + candidate.length,
label: "[encoded payload hidden]",
});
}
}
}
Expand Down
29 changes: 17 additions & 12 deletions extension/src/rules/schema-trust-sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,20 @@ function microdataTypeNames(itemtype: string): string[] {
// Find the descendant `[itemprop=NAME]` that belongs to this item's
// scope — i.e. is not nested inside a deeper `[itemscope]` first. The
// microdata spec scopes itemprops to their nearest ancestor itemscope.
// Nearest ancestor with an `[itemscope]` attribute, or null if none.
function nearestItemscope(element: Element): Element | null {
for (
let parent = element.parentElement;
parent !== null;
parent = parent.parentElement
) {
if (parent.hasAttribute("itemscope")) {
return parent;
}
}
return null;
}

function scopedDescendants(scope: Element, itempropName: string): Element[] {
const out: Element[] = [];
// Caller is the rule's internal code path; itempropName is one of a
Expand All @@ -190,18 +204,9 @@ function scopedDescendants(scope: Element, itempropName: string): Element[] {
if (candidate === scope) {
continue;
}
// Walk up to find the nearest itemscope ancestor; if it isn't us,
// the itemprop belongs to a nested scope.
let parent = candidate.parentElement;
let nearestScope: Element | null = null;
while (parent !== null) {
if (parent.hasAttribute("itemscope")) {
nearestScope = parent;
break;
}
parent = parent.parentElement;
}
if (nearestScope === scope) {
// The itemprop belongs to us only if our scope is its nearest
// itemscope ancestor; otherwise it's claimed by a deeper scope.
if (nearestItemscope(candidate) === scope) {
out.push(candidate);
}
}
Expand Down
Loading