Skip to content

Commit f10327c

Browse files
committed
Fixes #15: Fixed blur infinite loop by adding an optimization mode.
1 parent be4761a commit f10327c

File tree

5 files changed

+167
-20
lines changed

5 files changed

+167
-20
lines changed

docs/qa-testplan.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## QA test plan
2+
3+
### Static site
4+
5+
Use https://fleetdm.com/ or similar for testing.
6+
7+
- Enable/disable blurring
8+
- Enable/disable/change a blur literal
9+
- Ensure the secret doesn't appear (flicker) during reload.
10+
11+
### Static file
12+
13+
Use https://victoronsoftware.com/sitemap.xml or similar for testing.
14+
15+
- Enable/disable blurring
16+
- Enable/disable/change a blur literal
17+
- Note: the secret may appear (flicker) during reload because this is just a file and not an actual webpage.
18+
19+
### Dynamic site
20+
21+
Use Tines or similar for testing.
22+
23+
- Create a Webhook action and blur the webhook secret.

src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ export const MODES = [
1212
color: "#008C20",
1313
},
1414
]
15-
export const NUMBER_OF_ITEMS = 10
15+
export const NUMBER_OF_LITERALS = 10

src/content.ts

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { NUMBER_OF_ITEMS} from "./constants"
1+
import { NUMBER_OF_LITERALS} from "./constants"
2+
import Optimizer from "./optimizer"
23

34
const blurFilter = "blur(0.343em)" // This unique filter value identifies the OpenBlur filter.
45
const tagsNotToBlur = ["HEAD", "SCRIPT", "STYLE", "loc"]
@@ -8,6 +9,12 @@ let enabled = true
89
let bodyHidden = true
910
let doFullScan = false
1011

12+
// Performance optimization. The performance optimization mode is enabled when we blur a lot of elements in a short period of time.
13+
const maxBlursCount = 100
14+
let blursCount = maxBlursCount
15+
const performanceOptimizationResetMs = 5 *1000
16+
let performanceOptimizationMode = false
17+
1118
console.debug("OpenBlur content script loaded")
1219

1320
function unhideBody() {
@@ -18,28 +25,36 @@ function unhideBody() {
1825
}
1926

2027
function processInputElement(input: HTMLInputElement | HTMLTextAreaElement) {
28+
let blurTarget : HTMLElement = input
29+
if (performanceOptimizationMode && input.parentElement instanceof HTMLElement) {
30+
// In performance optimization mode, we may blur the parent.
31+
const grandParent = input.parentElement as HTMLElement
32+
if (grandParent.style && grandParent.style.filter.includes(blurFilter)) {
33+
// Treat the grandparent as the parent.
34+
blurTarget = grandParent
35+
}
36+
}
2137
let text = input.value || input.getAttribute("value") || ""
22-
if (input.style.filter.includes(blurFilter)) {
38+
if (blurTarget.style.filter.includes(blurFilter)) {
2339
// Already blurred
2440
if (!enabled) {
2541
// We remove the blur filter if the extension is disabled.
26-
input.style.filter = input.style.filter.replace(blurFilter, "")
42+
unblurElement(blurTarget)
2743
return
2844
}
2945
const blurNeeded = contentToBlur.some((content) => {
3046
return text.includes(content);
3147
})
3248
if (!blurNeeded) {
33-
input.style.filter = input.style.filter.replace(blurFilter, "")
49+
unblurElement(blurTarget)
3450
}
3551
return
36-
}
37-
if (enabled && text.length > 0) {
52+
} else if (enabled && text.length > 0) {
3853
const blurNeeded = contentToBlur.some((content) => {
3954
return text.includes(content);
4055
})
4156
if (blurNeeded) {
42-
blurElement(input)
57+
blurElement(blurTarget)
4358
}
4459
}
4560
}
@@ -52,14 +67,22 @@ function processNode(node: Node) {
5267
Array.from(node.childNodes).forEach(processNode)
5368
}
5469
if (node.nodeType === Node.TEXT_NODE && node.textContent !== null && node.textContent.trim().length > 0) {
55-
const parent = node.parentElement
70+
let parent = node.parentElement
5671
if (parent !== null && parent.style) {
5772
const text = node.textContent!
73+
if (performanceOptimizationMode && parent.parentElement instanceof HTMLElement) {
74+
// In performance optimization mode, we may blur the parent's parent.
75+
const grandParent = parent.parentElement as HTMLElement
76+
if (grandParent.style && grandParent.style.filter.includes(blurFilter)) {
77+
// Treat the grandparent as the parent.
78+
parent = grandParent
79+
}
80+
}
5881
if (parent.style.filter.includes(blurFilter)) {
5982
// Already blurred
6083
if (!enabled) {
6184
// We remove the blur filter if the extension is disabled.
62-
parent.style.filter = parent.style.filter.replace(blurFilter, "")
85+
unblurElement(parent)
6386
return
6487
}
6588
if (doFullScan) {
@@ -68,12 +91,11 @@ function processNode(node: Node) {
6891
return text.includes(content);
6992
})
7093
if (!blurNeeded) {
71-
parent.style.filter = parent.style.filter.replace(blurFilter, "")
94+
unblurElement(parent)
7295
}
7396
}
7497
return
75-
}
76-
if (enabled) {
98+
} else if (enabled) {
7799
const blurNeeded = contentToBlur.some((content) => {
78100
return text.includes(content);
79101
})
@@ -99,14 +121,37 @@ function processNode(node: Node) {
99121
}
100122

101123
function blurElement(elem: HTMLElement) {
102-
if (elem.style.filter.length == 0) {
103-
elem.style.filter = blurFilter
124+
let blurTarget: HTMLElement = elem
125+
if (performanceOptimizationMode) {
126+
const ok = Optimizer.addElement(elem)
127+
if (!ok) {
128+
blurTarget = elem.parentElement as HTMLElement
129+
void Optimizer.addElement(elem)
130+
}
131+
}
132+
if (blurTarget.style.filter.length == 0) {
133+
blurTarget.style.filter = blurFilter
104134
} else {
105135
// The element already has a filter. Append our blur filter to the existing filter.
106136
// We assume that the semicolon(;) is never present in the filter string. This has been the case in our limited testing.
107-
elem.style.filter += ` ${blurFilter}`
137+
blurTarget.style.filter += ` ${blurFilter}`
108138
}
109139
console.debug("OpenBlur blurred element id:%s, class:%s, tag:%s, text:%s", elem.id, elem.className, elem.tagName, elem.textContent)
140+
blursCount--
141+
if (blursCount <= 0) {
142+
if (!performanceOptimizationMode) {
143+
console.debug("OpenBlur performance optimization mode enabled")
144+
performanceOptimizationMode = true
145+
}
146+
}
147+
}
148+
149+
function unblurElement(elem: HTMLElement) {
150+
elem.style.filter = elem.style.filter.replace(blurFilter, "")
151+
if (performanceOptimizationMode) {
152+
Optimizer.removeElement(elem)
153+
}
154+
console.debug("OpenBlur unblurred element id:%s, class:%s, tag:%s, text:%s", elem.id, elem.className, elem.tagName, elem.textContent)
110155
}
111156

112157
const observer = new MutationObserver((mutations) => {
@@ -147,11 +192,15 @@ function disconnectInputs() {
147192
function disconnect() {
148193
observer.disconnect()
149194
disconnectInputs()
195+
if (performanceOptimizationMode) {
196+
Optimizer.clear()
197+
performanceOptimizationMode = false
198+
}
150199
}
151200

152201
function setLiterals(literals: string[]) {
153202
contentToBlur.length = 0
154-
for (let i = 0; i < NUMBER_OF_ITEMS; i++) {
203+
for (let i = 0; i < NUMBER_OF_LITERALS; i++) {
155204
const item: string = literals[i]
156205
if (item && item.trim().length > 0) {
157206
contentToBlur.push(item.trim())
@@ -191,3 +240,7 @@ chrome.runtime.onMessage.addListener((request, _sender, _sendResponse) => {
191240
setLiterals(request.literals)
192241
}
193242
})
243+
244+
setInterval(() => {
245+
blursCount = maxBlursCount
246+
}, performanceOptimizationResetMs)

src/optimizer.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// This file tracks what has been blurred on the page.
2+
3+
const countSlack = 5
4+
const recentMs = 300
5+
6+
type BlurredItem = {
7+
blurredAt: Date,
8+
recentCount: number,
9+
}
10+
11+
const blurredMap = new Map<string, BlurredItem>()
12+
13+
function getUniquePath(element: HTMLElement): string {
14+
if (element.id && element.id !== "") {
15+
return `id("${element.id}")`
16+
}
17+
if (element.parentElement === null) {
18+
return element.tagName;
19+
}
20+
21+
let siblingIndex= 0
22+
const siblings = element.parentElement.childNodes
23+
// If there is only one sibling, then no need to add an index
24+
for (let i = 1; i < siblings.length; i++) {
25+
if (siblings[i] instanceof HTMLElement) {
26+
const sibling= siblings[i] as HTMLElement
27+
if (sibling === element) {
28+
return `${getUniquePath(element.parentElement)}/${element.tagName}[${siblingIndex}]`
29+
}
30+
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === element.tagName) {
31+
siblingIndex++;
32+
}
33+
}
34+
}
35+
return `${getUniquePath(element.parentNode as HTMLElement)}/${element.tagName}`
36+
}
37+
38+
const Optimizer = {
39+
addElement: (elem: HTMLElement) : boolean => {
40+
const path = getUniquePath(elem)
41+
const now = new Date()
42+
if (blurredMap.has(path)) {
43+
// Element has already been blurred
44+
const item = blurredMap.get(path)!
45+
const diff = now.getTime() - item.blurredAt.getTime()
46+
if (diff < recentMs) {
47+
if (item.recentCount > countSlack) {
48+
// Element has been blurred too many times recently. This is a performance issue.
49+
return false
50+
}
51+
item.blurredAt = now
52+
item.recentCount++
53+
} else {
54+
item.blurredAt = now
55+
item.recentCount = 1
56+
}
57+
} else {
58+
blurredMap.set(path, { blurredAt: now, recentCount: 1 })
59+
}
60+
return true
61+
},
62+
removeElement: (elem: HTMLElement) => {
63+
const path = getUniquePath(elem)
64+
blurredMap.delete(path)
65+
},
66+
clear: () => {
67+
blurredMap.clear()
68+
},
69+
}
70+
71+
export default Optimizer

src/popup.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MODES, NUMBER_OF_ITEMS } from "./constants"
1+
import { MODES, NUMBER_OF_LITERALS } from "./constants"
22

33
console.debug("OpenBlur popup script loaded")
44

@@ -11,8 +11,8 @@ chrome.storage.sync.get(null, (data) => {
1111
void chrome.action.setBadgeBackgroundColor({color: mode.color})
1212
let literals: string[] = data.literals || []
1313

14-
// Loop over NUMBER_OF_ITEMS elements and listen to each one.
15-
for (let i = 0; i < NUMBER_OF_ITEMS; i++) {
14+
// Loop over NUMBER_OF_LITERALS elements and listen to each one.
15+
for (let i = 0; i < NUMBER_OF_LITERALS; i++) {
1616
const input = document.getElementById(`item_${i}`) as HTMLInputElement
1717
input.value = literals[i] || ""
1818
input.addEventListener("change", async (event) => {

0 commit comments

Comments
 (0)