Skip to content
C.W.K.
Lesson 02 of 05 · published

Pop-ups and Redirects Stay in Their Identity

~14 min · webkit, identities, pop-ups, wkuidelegate

Level 0New Tab
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

Where a second account leaks

Keeping two tabs apart is half the job. Pages open more pages: a sign-in window, a link with target="_blank", a result that opens in a new tab. If any of them landed in a different store, the second account would quietly become the first, or a sign-in finished in a pop-up would never reach the tab that asked for it. Architecture invariant 7 closes all of these in two sentences: "Links, pop-ups and redirects opened from a tab stay in its identity. Nothing moves cookies between identities."

Voyager meets that rule in a different way on each path a new page can take.

Redirects: nothing to decide

A redirect, whether the server sends it or a script sets location, is a navigation inside the same web view. A WKWebView takes its configuration, data store included, when it is made, and a live view's store cannot be swapped. So a redirect cannot leave its identity, and Voyager has no code for it. The one place a navigation needs a different configuration is between an extension's own page and an ordinary one, and there the page is replaced with a new view (track 8).

Pop-ups: WebKit hands over the configuration

When a page calls window.open or follows a target="_blank" link, WebKit asks its UI delegate for a new web view and passes a configuration along. Apple's documentation states the condition: "The web view returned must be created with the specified configuration." That configuration carries the opener's data store, and with it the link back to the opener that a sign-in pop-up needs in order to report to the page that opened it.

Voyager's child builds the new page from exactly that configuration and labels it with the opener's identity. Its comment says why no lookup is needed: the child is in the opener's identity "by construction". It then asks the parent whether to keep the page. A refusal closes the child and returns nil, which is what the page sees as window.open returning null.

The parent checks anyway

BrowserTab hands the child to its window, and the window's adopt refuses any child whose identity differs from its opener's. With the WebKit child that guard never fires, since the identity was copied from the opener a few lines earlier. It is there because the parent cannot see WebKit. The rule has to exist in the parent's own vocabulary, where the fake engine can test it without a browser.

aPageOpenedByAPageJoinsOnlyInTheSameIdentity does exactly that. It hands an opener a "stranger" page with a fresh identity; the window refuses it and still holds one tab. A child in the opener's identity becomes the next tab and comes to the front. One more fact from the code: the child always becomes a tab right after its opener, even when the page asked for a sized window. The request records wantsOwnWindow from the window features, and the window model does not act on it today.

Links in a new tab

Command-click, a middle click and the context menu's "Open Link in New Tab" never touch WebKit's pop-up path. For the two clicks, the child cancels the navigation and tells the parent wantsNewTabAt, and the tab answers by opening a tab after itself in its own identity. The menu command never reaches the engine at all: the shell calls model.openTab itself, naming the tab's identity, placing the new tab after it and leaving it behind. Command alone opens the link behind, Shift brings it to the front, and the middle button opens it behind, as Chrome does. aLinkedTabKeepsItsOpenersIdentity opens a link from a tab in a "Work" identity and expects two Work tabs. Duplicating a tab keeps its identity too.

Only a click opens a window

Every configuration the engine builds sets javaScriptCanOpenWindowsAutomatically to false, which Apple defines as whether JavaScript may open windows "without user interaction". The comment beside it: "A page opens a window only when Dad's own click asked for one."

The real-WebKit test aPageOpenedByAPageIsInItsIdentityAndCanBeRefused first checks that the preference is off, then turns it on in its own configuration "to stand in for the click". It opens a 400 by 300 window and checks three things: the child's identity is the opener's, the child's data store is the very same object as the opener's (===), and wantsOwnWindow is true. The child then closes itself with window.close() and the parent hears it. Last, with the parent refusing children, window.open returns null.

One seam, named

Not every way out of a tab carries its identity. The context menu's "Open Link in New Window" opens a fresh window, and a fresh window's first tab takes the default identity. A link from a Work tab opened that way arrives signed in as whoever the default is. The implementation plan still lists "open a link or reopen a tab in another identity" and a default identity per site as open items. A tab dragged into another window does keep its identity, because a moved tab carries its saved identity along; a drag never lands in a window of the other kind, private or normal, and lesson 3 shows how the model guards that line anyway.

Principle: List every path a new page can take before you trust the rule. Three of Voyager's paths keep the identity by construction or by a guard; the fourth was found by reading the menu code.

Code

cwkVoyager, Sources/VoyagerWebKit/WebKitPage.swift: a pop-up is built from the configuration WebKit hands over (verbatim, lines 437-453)·swift
    func webView(
        _ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
        for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures
    ) -> WKWebView? {
        guard let delegate else { return nil }
        // WebKit's configuration carries the opener and its data store, so the
        // child is in the opener's identity by construction.
        let child = WebKitPage(engine: engine, identity: identity, configuration: configuration, restoring: nil, extensionID: extensionID)
        let request = NewPageRequest(
            url: navigationAction.request.url,
            wantsOwnWindow: windowFeatures.width != nil || windowFeatures.height != nil)
        guard delegate.page(self, didOpen: child, for: request) else {
            child.close()
            return nil
        }
        return child.webView
    }
cwkVoyager, Sources/VoyagerCore/BrowserWindowModel.swift: the window adopts a child only in its opener's identity (verbatim, lines 315-325)·swift
    /// A page the engine made for one of this window's pages. It joins the
    /// window only in its opener's identity.
    func adopt(_ child: any EnginePage, openedBy opener: BrowserTab) -> Bool {
        guard child.identity == opener.identity else { return false }
        child.zoom = services.settings.value.pageZoom
        let tab = BrowserTab(adopting: child)
        insert(tab, after: opener)
        selectedID = tab.id
        sessionChanged()
        return true
    }
cwkVoyager, Sources/VoyagerCore/BrowserTab.swift: both ways a page opens another, answered in the tab's identity (verbatim, lines 393-399)·swift
    public func page(_ page: any EnginePage, didOpen child: any EnginePage, for request: NewPageRequest) -> Bool {
        window?.adopt(child, openedBy: self) ?? false
    }

    public func page(_ page: any EnginePage, wantsNewTabAt url: URL, inBackground: Bool) {
        window?.openTab(url: url, identity: identity, after: self, select: !inBackground)
    }

External links

Exercise

In an app that hosts WKWebView with more than one data store, implement webView(_:createWebViewWith:for:windowFeatures:) so the new view is built from the configuration WebKit passes. Write a test that loads a page in a non-default store, opens a window from script, and asserts that the child's websiteDataStore is the same object as the opener's. Then list every way your app's UI can open a page from another page (menu items, keyboard shortcuts, drag and drop, a share action) and write down which store each one uses.
Hint
Apple documents javaScriptCanOpenWindowsAutomatically as true by default on macOS and false on iOS. Voyager sets it to false everywhere, so its test turns it on for one configuration to open a window from script without a click; know which value your own app runs with. The list is the part that finds bugs: a menu command that opens a new window usually builds its first tab from the default, not from the page it came from.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.