← All writing

Adding a Blocked-URL Callback to WKNavigationDelegate

How I added a WebKit callback to report blocked URLs in my browser, from WebCore through IPC to Swift.

My browser uses over 100,000 network-blocking rules. I wanted to see which URLs they blocked, so I added a callback to WKNavigationDelegate in my WebKit fork.

Detecting Blocked URLs

CachedResourceLoader.cpp contains a function called requestResource which handles resource loading and performs content-blocking checks as part of that process

I wanted a delegate function in swift which I can read these URLs

The Function performed:

const auto& resourceRequest = request.resourceRequest();

resourceRequest contains a url() function so I converted it to a WTF::String and forwarded it through WebCore’s Chrome interface:

String urlStr = resourceRequest.url().string();
page->chrome().client().didJustBlock(urlStr);

this calls a virtual method named didJustBlock defined in ChromeClient.h:

virtual void didJustBlock(String&) { };

The client we needed to implement this on was WebChromeClient.cpp. WebChromeClient is a child class of ChromeClient so we can just override its function in WebChromeClient.h:

void didJustBlock(String&) override;

WebChromeClient.cpp does:

void WebChromeClient::didJustBlock(String& url)
{
    if (RefPtr page = m_page.get()) {
        page->send(Messages::WebPageProxy::DidJustBlock(url));
    }
}

IPC - WebProcess to UIProcess

when the WebChromeClient.cpp did:

page->send(Messages::WebPageProxy::DidJustBlock(url));

Messages::WebPageProxy::DidJustBlock(url) is a message created by a .in file WebPageProxy.messages.in We can just write

DidJustBlock(String url)

And while compiling, it generates the message in WebPageProxyMessages.h under Messages::WebPageProxy

This is now when the IPC message starts, WebPageProxy.h must define the function:

void didJustBlock(String&&);

The IPC Part is interesting

an IPC Message is sent from WebChromeClient.cpp using page->send(...), WebKit’s generated IPC Machinery serializes and delivers the message to WebPageProxy in the UI process, where didJustBlock(String&&) handles it

Runtime Polymorphism

before we implement the didJustBlock(String&&) inside of the WebPageProxy we create the function that it can call inside of APINavigationClient.h: we implement:

virtual void didJustBlock(WebKit::WebPageProxy&, Ref<API::String>&) { };

This means any child classes can implement this, and its useful to us in the WebPageProxy.cpp where can now implement the function didJustBlock:

void WebPageProxy::didJustBlock(String&& url)
{
    Ref<API::String> toSendUrl = API::String::create(WTF::move(url));
    m_navigationClient->didJustBlock(*this, toSendUrl);
}

m_navigationClient is a unique reference to the base class API::NavigationClient. m_navigationClient is defined like this in the header:

UniqueRef<API::NavigationClient> m_navigationClient;

the API:: only exists because NavigationClient is under namespace API{}

WKWebView.mm sets the m_navigationClient when we assign a WKNavigationDelegate:

- (void)setNavigationDelegate:(id <WKNavigationDelegate>)navigationDelegate
{
    _page->setNavigationClient(_navigationState->createNavigationClient());
    _navigationState->setNavigationDelegate(navigationDelegate);
}

Which calls:

void WebPageProxy::setNavigationClient(UniqueRef<API::NavigationClient>&& navigationClient)
{
    m_navigationClient = WTF::move(navigationClient);
}

_navigationState is just: NavigationState its create function looks like this:

UniqueRef<API::NavigationClient> NavigationState::createNavigationClient()
{
    return makeUniqueRef<NavigationClient>(*this);
}

This is allowed because NavigationState in its private header does this:

class NavigationClient final : public API::NavigationClient {

so now we know that that WebPageProxy’s m_navigationClient now holds a NavigationState::NavigationClient

We had just added the function:

virtual void didJustBlock(WebKit::WebPageProxy&, Ref<API::String>&) { };

inside of API::NavigationClient

because NavigationState::NavigationClient inherits from API::NavigationClient it can override the virtual function defined in the base class:

void didJustBlock(WebPageProxy&, Ref<API::String>&) override;

So before we implement this inside of NavigationState.mm we need to make sure that the delegate function can call this.

Implementing The Delegate Function

so in the WKNavigationDelegate.h we can write this:

- (void)webView:(WKWebView *)webView didJustBlock:(NSURL *)blockedURL;

This allows a user to implement this function when they assign the WKNavigationDelegate

When we do:

webView.navigationDelegate = self

in swift This implicitly calls:

NavigationState::setNavigationDelegate(id<WKNavigationDelegate> delegate)

This function assigns delegate functions

It works like this:

Swift
  webView.navigationDelegate = self
              |
              v
WKWebView.mm
  -setNavigationDelegate:
              |
              +--> WebPageProxy::setNavigationClient(...)
              |
              +--> NavigationState::setNavigationDelegate(...)

So for our implementation to work, we go to the m_navigationDelegateMethods struct inside of NavigationState and add a flag:

bool webViewDidJustBlock: 1;

Now we can do this in setNavigationDelegate:

m_navigationDelegateMethods.webViewDidJustBlock = [delegate respondsToSelector:@selector(webView:didJustBlock:)];

From earlier we just override’d the function call:

void didJustBlock(WebPageProxy&, Ref<API::String>&) override;

We can now implement it in NavigationState.mm

void NavigationState::NavigationClient::didJustBlock(WebKit::WebPageProxy&, Ref<API::String>& url)
{
    RefPtr navigationState = m_navigationState.get();
    if (!navigationState)
        return;

    auto navigationDelegate = navigationState->navigationDelegate();
    if (!navigationDelegate)
        return;

    if (navigationState->m_navigationDelegateMethods.webViewDidJustBlock) {

        NSURL *nsURL = [NSURL URLWithString:url->string().createNSString().autorelease()];
        if (!nsURL)
            return;

        [navigationDelegate webView:navigationState->webView().get() didJustBlock:(nsURL)];
    }
}

Using it in Swift

This callback requires building and running against my ComfyWebKit fork.

My browser’s navigation delegate receives blocked URLs like this:

func webView(_ webView: WKWebView, didJustBlock blockedURL: URL) {
    print("WEB VIEW DID JUST BLOCK: \(blockedURL.absoluteString)")
}