As we know, we can use WKContentRuleList to block url requests/cookies or perform other actions in WKWebView. Is there any way that we can know what has been blocked by the WKWebView based on that WKContentRuleList?
I have some level of certainty that there isn't a simple way to just retrieve this information using public APIs. As such, I've put together a javascript work-around that's "good enough" solution for my purposes. It attempts to extract the resources from the parsed html and then it compares them to the loaded resources, as obtained from the window.performance module. The main caveat is that some resource types aren't handled at all, while others are probably missed.
Obviously, it should be called after the page has fully loaded what it's going to load. Usually, this would be done from the 'webViewDidFinishNavigation' delegate method. The provided completion argument is closure that is called with an array of the blocked resources as the single parameter.
This first part is a function to build the javascript to extract the resources from the page. Stackoverflow seems to format things better with this split out.
private static func buildResourceInfoJavascript() -> String {
let script = """
function extractUrls( fromCss ) {
let matches = fromCss.match(/url\\(.+?\\)/g);
if( !matches ) {
return [] ;
}
let urls = matches.map(url => url.replace(/url\\(['\\"]?(.+?)['\\"]?\\)/g, "$1"));
return urls;
}
function getPageResources() {
let pageResources = [...document.images].map(x => x.src);
pageResources = [...pageResources, ...[...document.scripts].map(x => x.src) ] ;
pageResources = [...pageResources, ...[...document.getElementsByTagName("link")].map(x => x.href) ];
[...document.styleSheets].forEach(sheet => {
if( !sheet.cssRules ) {
return ;
}
[...sheet.cssRules].forEach(rule => {
pageResources = [...pageResources, ...extractUrls( rule.cssText )];
} );
});
let inlineStyles = document.querySelectorAll( '*[style]') ;
[...inlineStyles].forEach(x => {
pageResources = [...pageResources, ...extractUrls( x.getAttributeNode("style").value )];
}) ;
let backgrounds = document.querySelectorAll( 'td[background], tr[background], table[background]') ;
[...backgrounds].forEach(x => {
pageResources.push( x.getAttributeNode("background").value );
}) ;
return pageResources.filter(x => (x != null && x != '') );
}
let pageResources = getPageResources() ;
let loadedResources = window.performance.getEntriesByType('resource').map(x => x.name );
let resourceInfo = {
'pageResources' : pageResources,
'loadedResources' : loadedResources.filter(x => (x != null && x != '') ),
};
JSON.stringify(resourceInfo);
"""
return script
}
This next part is the function that is called from the didFinishNavigation delegate.
public static func getBlockedResourcesAsync( fromWebView:WKWebView, completion:@escaping (([String]) -> Void)) {
let script = buildResourceInfoJavascript()
fromWebView.evaluateJavaScript(script) { (results, error) in
guard let resultsData = (results as? String)?.data(using: .utf8) else {
NSLog("No results for getBlockedResources" )
completion( [] )
return
}
do {
let resourceInfo = try JSONSerialization.jsonObject(with: resultsData) as? [String:[String]] ?? [:]
let pageResources = Array(Set(resourceInfo["pageResources"] ?? []) )
let loadedResources = Array(Set( resourceInfo["loadedResources"] ?? []) )
let blockedResources = pageResources.filter { !loadedResources.contains($0) }
let unrecognizedResources = loadedResources.filter { !pageResources.contains($0) }
if unrecognizedResources.count > 0 {
NSLog("Didn't recognized resources \(unrecognizedResources)" )
}
completion( blockedResources )
}
catch let err {
NSLog("JSON decoding failed: \(err.localizedDescription)" )
completion([])
return
}
}
}