How to know what's blocked by WKContentRuleList

1.3k views Asked by At

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?

3

There are 3 answers

0
Rich Waters On BEST ANSWER

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
        }
    }
}

3
Nandish On

Do you mean you want to get what all are the rules in the list or you want to lookup the rule list? Then you can use below api's of WKContentRuleListStore https://developer.apple.com/documentation/webkit/wkcontentruleliststore

// Gets the identifiers for all rules lists in the store.
func getAvailableContentRuleListIdentifiers((([String]?) -> Void)!)

// Searches for a specific rules list in the store.
func lookUpContentRuleList(forIdentifier: String!, completionHandler: 
   ((WKContentRuleList?, Error?) -> Void)!)

You can also refer the answer for question below, if you require more details: block ads from url loaded in WKWebView

UPDATE: Check this thread if it will be of any help How to show the inspector within your WKWebView based desktop app?

0
JC Mateo On

You can do it with the WebKit SDK. Using WKScriptMessageHandler : https://developer.apple.com/documentation/webkit/wkscriptmessagehandler/1396222-usercontentcontroller

You can find a sample of this technic in the open source project iOS browser DuckDuckGo : https://github.com/duckduckgo/iOS

Have a look to this file : https://github.com/duckduckgo/iOS/blob/develop/Core/ContentBlockerRulesUserScript.swift