fix(privacy): Procedural filters matching against dynamically added child elements (#29139)
* For procedural filtering, check child nodes when failing to match root element to the selector. * Add browser test for element with child procedural filter match. * Add iOS test for element with child procedural filter match. * Remove outdated procedural filters script sync comment after landing https://github.com/brave/brave-browser/issues/43262.
This commit is contained in:
@@ -2761,6 +2761,27 @@ IN_PROC_BROWSER_TEST_F(AdBlockServiceTest, ProceduralFilterHasText) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test procedural filters matching child of dynamically added element
|
||||
IN_PROC_BROWSER_TEST_F(AdBlockServiceTest,
|
||||
ProceduralFilterDynamicAddedChildHasText) {
|
||||
UpdateAdBlockInstanceWithRules(
|
||||
"a.com##.procedural-filter-child-node-class:has-text(View in App)");
|
||||
|
||||
GURL tab_url =
|
||||
embedded_test_server()->GetURL("a.com", "/cosmetic_filtering.html");
|
||||
NavigateToURL(tab_url);
|
||||
|
||||
content::WebContents* contents = web_contents();
|
||||
|
||||
auto result = EvalJs(contents,
|
||||
R"(
|
||||
addElementWithChildDynamically();
|
||||
waitCSSSelector('.procedural-filter-child-node-class', 'display', 'none')
|
||||
)");
|
||||
ASSERT_TRUE(result.error.empty());
|
||||
EXPECT_EQ(base::Value(true), result.value);
|
||||
}
|
||||
|
||||
// Test `matches-attr` procedural filters
|
||||
IN_PROC_BROWSER_TEST_F(AdBlockServiceTest, ProceduralFilterMatchesAttr) {
|
||||
UpdateAdBlockInstanceWithRules(
|
||||
|
||||
@@ -299,9 +299,13 @@ const onMutations = (mutations: MutationRecord[], observer: MutationObserver) =>
|
||||
if (CC.hasProceduralActions) {
|
||||
const addedElements : Element[] = [];
|
||||
mutations.forEach(mutation =>
|
||||
mutation.addedNodes.length !== 0 && mutation.addedNodes.forEach(n =>
|
||||
mutation.addedNodes.length !== 0 && mutation.addedNodes.forEach(n => {
|
||||
n.nodeType === Node.ELEMENT_NODE && addedElements.push(n as Element)
|
||||
)
|
||||
const childNodes = (n as Element).querySelectorAll('*')
|
||||
childNodes.length !== 0 && childNodes.forEach(c => {
|
||||
c.nodeType === Node.ELEMENT_NODE && addedElements.push(c as Element)
|
||||
})
|
||||
})
|
||||
)
|
||||
if (addedElements.length !== 0) {
|
||||
executeProceduralActions(addedElements);
|
||||
|
||||
@@ -357,9 +357,15 @@ import { applyCompiledSelector, compileProceduralSelector } from './procedural_f
|
||||
if (CC.hasProceduralActions) {
|
||||
const addedElements = [];
|
||||
mutations.forEach(mutation =>
|
||||
mutation.addedNodes.length !== 0 && mutation.addedNodes.forEach(n =>
|
||||
n.nodeType === Node.ELEMENT_NODE && addedElements.push(n)
|
||||
)
|
||||
mutation.addedNodes.length !== 0 && mutation.addedNodes.forEach(n => {
|
||||
if (n.nodeType === Node.ELEMENT_NODE) {
|
||||
addedElements.push(n)
|
||||
const childNodes = n.querySelectorAll('*')
|
||||
childNodes.length !== 0 && childNodes.forEach(c => {
|
||||
c.nodeType === Node.ELEMENT_NODE && addedElements.push(c)
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
if (addedElements.length !== 0) {
|
||||
executeProceduralActions(addedElements);
|
||||
|
||||
@@ -3,15 +3,6 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
/**
|
||||
*
|
||||
* This file should be kept up to date with
|
||||
* https://github.com/brave-experiments/procedural-filters-js
|
||||
* until the full implementation can be shared with iOS inside
|
||||
* the brave-core repo.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* src/declarations.d.ts
|
||||
@@ -209,13 +200,13 @@ const _nextSiblingElement = (element: HTMLElement): HTMLElement | null => {
|
||||
const _allChildren = (element: HTMLElement): HTMLElement[] => {
|
||||
return W.Array.from(element.children)
|
||||
.map(e => _asHTMLElement(e))
|
||||
.filter(e => e !== null) as HTMLElement[]
|
||||
.filter(e => e !== null)
|
||||
}
|
||||
|
||||
const _allChildrenRecursive = (element: HTMLElement): HTMLElement[] => {
|
||||
return W.Array.from(element.querySelectorAll(':scope *'))
|
||||
.map(e => _asHTMLElement(e))
|
||||
.filter(e => e !== null) as HTMLElement[]
|
||||
.filter(e => e !== null)
|
||||
}
|
||||
|
||||
const _stripCssOperator = (operator: string, selector: string) => {
|
||||
|
||||
@@ -134,6 +134,28 @@
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
const addElementWithChildDynamically = () => {
|
||||
let root = document.documentElement;
|
||||
const rootElement = document.createElement('div');
|
||||
rootElement.setAttribute('class', 'procedural-filter-root-node-class');
|
||||
|
||||
const childNode = document.createElement('div');
|
||||
childNode.setAttribute('class', 'procedural-filter-child-node-class');
|
||||
rootElement.appendChild(childNode);
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'View in App';
|
||||
childNode.appendChild(p);
|
||||
|
||||
const childNode2 = document.createElement('div');
|
||||
childNode2.setAttribute('class', 'procedural-filter-child-node-class');
|
||||
rootElement.appendChild(childNode2);
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'View in App';
|
||||
childNode2.appendChild(span);
|
||||
|
||||
root.appendChild(rootElement);
|
||||
}
|
||||
|
||||
const onload = () => {
|
||||
const localFrame = document.createElement('iframe');
|
||||
localFrame.id = 'local-iframe';
|
||||
@@ -149,7 +171,6 @@
|
||||
|
||||
setTimeout(displaySvalInBody, 100);
|
||||
setTimeout(displaySvalInLocalFrame, 200);
|
||||
setTimeout(addElementForProceduralFilter, 500);
|
||||
};
|
||||
window.onload = onload;
|
||||
</script>
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
'localFrameElement': results.localFrameElement,
|
||||
'hasTextDisplayIsNone': results.hasTextDisplayIsNone,
|
||||
'hasDisplayIsNone': results.hasDisplayIsNone,
|
||||
'delayedHasTextHidden': results.delayedHasTextHidden
|
||||
'delayedHasTextHidden': results.delayedHasTextHidden,
|
||||
'delayedChildHasTextHidden': results.delayedChildHasTextHidden,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,7 +48,8 @@
|
||||
localFrameElement: false,
|
||||
hasTextDisplayIsNone: false,
|
||||
hasDisplayIsNone: false,
|
||||
delayedHasTextHidden: false
|
||||
delayedHasTextHidden: false,
|
||||
delayedChildHasTextHidden: []
|
||||
}
|
||||
|
||||
elements.forEach((node) => {
|
||||
@@ -112,6 +114,21 @@
|
||||
}
|
||||
})
|
||||
|
||||
const elementsWithClass = document.querySelectorAll('[class]')
|
||||
elementsWithClass.forEach((node) => {
|
||||
if (!node.hasAttribute('class')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (node.getAttribute('class') === 'procedural-filter-child-node-class') {
|
||||
const nodeDisplay = window.getComputedStyle(node).display
|
||||
// 2 elements have this class, we want to test both their display values
|
||||
const delayedChildHasTextHidden = results.delayedChildHasTextHidden
|
||||
delayedChildHasTextHidden.push(nodeDisplay === 'none')
|
||||
results.delayedChildHasTextHidden = delayedChildHasTextHidden
|
||||
}
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ final class ScriptExecutionTests: XCTestCase {
|
||||
let hasTextDisplayIsNone: Bool
|
||||
let hasDisplayIsNone: Bool
|
||||
let delayedHasTextHidden: Bool
|
||||
let delayedChildHasTextHidden: [Bool]
|
||||
}
|
||||
|
||||
override class func setUp() {
|
||||
@@ -263,6 +264,7 @@ final class ScriptExecutionTests: XCTestCase {
|
||||
"brave.com###test-has-text:has-text(hide me)",
|
||||
"brave.com###test-has:has(a.banner-link)",
|
||||
"brave.com###test-delayed-has-text:has-text(hide me)",
|
||||
"brave.com##.procedural-filter-child-node-class:has-text(View in App)",
|
||||
].joined(separator: "\n")
|
||||
)
|
||||
let cosmeticFilterModel = try engine.cosmeticFilterModel(forFrameURL: siteURL)!
|
||||
@@ -407,6 +409,12 @@ final class ScriptExecutionTests: XCTestCase {
|
||||
// Now wait for the pump which takes a few seconds (The pump unhides 1st party elements).
|
||||
try await Task.sleep(seconds: 5)
|
||||
|
||||
let addDynamicElementsJavascript = """
|
||||
addElementForProceduralFilter();
|
||||
addElementWithChildDynamically();
|
||||
"""
|
||||
try await viewController.webView.evaluateJavaScript(addDynamicElementsJavascript)
|
||||
|
||||
// Execute a script that will test the cosmetic filters page
|
||||
let testURL = Bundle.module.url(forResource: "cosmetic-filter-tests", withExtension: "js")!
|
||||
let source = try String(contentsOf: testURL)
|
||||
@@ -469,6 +477,13 @@ final class ScriptExecutionTests: XCTestCase {
|
||||
XCTAssertTrue(resultsAfterPump?.hasTextDisplayIsNone ?? false)
|
||||
XCTAssertTrue(resultsAfterPump?.hasDisplayIsNone ?? false)
|
||||
XCTAssertTrue(resultsAfterPump?.delayedHasTextHidden ?? false)
|
||||
if let delayedChildHasTextHidden = resultsAfterPump?.delayedChildHasTextHidden {
|
||||
// expecting 2 elements with the same id to be hidden
|
||||
XCTAssertEqual(delayedChildHasTextHidden.count, 2)
|
||||
XCTAssertTrue(delayedChildHasTextHidden.allSatisfy({ $0 }))
|
||||
} else {
|
||||
XCTFail("delayedChildHasTextHidden missing in results")
|
||||
}
|
||||
// Test for local frames
|
||||
XCTAssertTrue(resultsAfterPump?.localFrameElement ?? false)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,28 @@ function addElementsDynamically() {
|
||||
}
|
||||
}
|
||||
|
||||
function addElementWithChildDynamically() {
|
||||
let root = document.documentElement;
|
||||
const rootElement = document.createElement('div');
|
||||
rootElement.setAttribute('class', 'procedural-filter-root-node-class');
|
||||
|
||||
const childNode = document.createElement('div');
|
||||
childNode.setAttribute('class', 'procedural-filter-child-node-class');
|
||||
rootElement.appendChild(childNode);
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'View in App';
|
||||
childNode.appendChild(p);
|
||||
|
||||
const childNode2 = document.createElement('div');
|
||||
childNode2.setAttribute('class', 'procedural-filter-child-node-class');
|
||||
rootElement.appendChild(childNode2);
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'View in App';
|
||||
childNode2.appendChild(span);
|
||||
|
||||
root.appendChild(rootElement);
|
||||
}
|
||||
|
||||
let didWait = false;
|
||||
|
||||
function check(selector, predicate) {
|
||||
|
||||
Reference in New Issue
Block a user