11 KiB
11 KiB
source, confidence, namespace, last_synced, alwaysApply
| source | confidence | namespace | last_synced | alwaysApply |
|---|---|---|---|---|
| ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/webkit-browser.mdc | 0.9 | work | 2026-04-28 | false |
WebKit & Browser Development Guidelines
WebView Configuration
Basic WebView Setup
import WebKit
class BrowserWebView {
private lazy var webView: WKWebView = {
let configuration = WKWebViewConfiguration()
// Enable JavaScript
configuration.preferences.javaScriptEnabled = true
// Set user agent
configuration.applicationNameForUserAgent = UserAgentManager.shared.userAgent
// Configure content blockers
configuration.userContentController = makeUserContentController()
// Enable developer extras in debug
#if DEBUG
configuration.preferences.setValue(true, forKey: "developerExtrasEnabled")
#endif
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.allowsBackForwardNavigationGestures = true
webView.allowsLinkPreview = true
return webView
}()
}
User Scripts Management
private func makeUserContentController() -> WKUserContentController {
let controller = WKUserContentController()
// Add content blocking scripts
let contentBlockingScript = WKUserScript(
source: ContentBlockingUserScript.source,
injectionTime: .atDocumentStart,
forMainFrameOnly: false
)
controller.addUserScript(contentBlockingScript)
// Add message handlers
controller.add(self, name: "duckduckgo")
return controller
}
Tab Management
Tab Model
class Tab: NSObject {
let id = UUID()
private(set) var url: URL?
private(set) var title: String?
private(set) var favicon: UIImage?
weak var webView: WKWebView?
weak var delegate: TabDelegate?
private var observations: Set<NSKeyValueObservation> = []
init(url: URL? = nil) {
self.url = url
super.init()
setupWebView()
}
private func setupWebView() {
let webView = WKWebView(frame: .zero, configuration: TabManager.shared.configuration)
self.webView = webView
// Observe properties
observations.insert(
webView.observe(\.url) { [weak self] _, _ in
self?.urlDidChange()
}
)
observations.insert(
webView.observe(\.title) { [weak self] webView, _ in
self?.title = webView.title
self?.delegate?.tab(self!, didUpdateTitle: webView.title)
}
)
observations.insert(
webView.observe(\.estimatedProgress) { [weak self] webView, _ in
self?.delegate?.tab(self!, didUpdateProgress: webView.estimatedProgress)
}
)
}
}
Tab Lifecycle
extension Tab {
func load(url: URL) {
let request = URLRequest(url: url)
webView?.load(request)
}
func reload() {
webView?.reload()
}
func stop() {
webView?.stopLoading()
}
func goBack() {
webView?.goBack()
}
func goForward() {
webView?.goForward()
}
func close() {
observations.forEach { $0.invalidate() }
observations.removeAll()
webView?.stopLoading()
webView?.removeFromSuperview()
webView = nil
}
}
Navigation Handling
Navigation Delegate
extension BrowserViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
let url = navigationAction.request.url
// Handle special URLs
if let url = url, URLSchemeHandler.shared.canHandle(url) {
URLSchemeHandler.shared.handle(url)
return .cancel
}
// Apply content blocking
if contentBlocker.shouldBlock(url) {
return .cancel
}
// Check for downloads
if shouldDownload(navigationAction) {
startDownload(from: navigationAction.request)
return .cancel
}
return .allow
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
updateProgressBar(animated: true)
updateNavigationButtons()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
hideProgressBar()
captureHistory()
updateFavicon()
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
handleNavigationError(error)
}
}
JavaScript Bridge
Message Handling
extension BrowserViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let dict = message.body as? [String: Any] else { return }
switch message.name {
case "duckduckgo":
handleDuckDuckGoMessage(dict)
case "autofill":
handleAutofillMessage(dict)
case "tracker":
handleTrackerMessage(dict)
default:
break
}
}
private func handleDuckDuckGoMessage(_ message: [String: Any]) {
guard let action = message["action"] as? String else { return }
switch action {
case "openSettings":
presentSettings()
case "reportBrokenSite":
presentBrokenSiteReport()
default:
break
}
}
}
JavaScript Injection
extension WKWebView {
func evaluateJavaScriptSafely(_ script: String) async throws -> Any? {
return try await withCheckedThrowingContinuation { continuation in
evaluateJavaScript(script) { result, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: result)
}
}
}
}
func injectPrivacyProtection() async {
let script = """
(function() {
// Override fingerprinting methods
const originalCanvas = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function() {
return "";
};
// Block tracking pixels
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.tagName === 'IMG' && isTrackingPixel(node.src)) {
node.remove();
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
"""
try? await evaluateJavaScriptSafely(script)
}
}
Cookie Management
Cookie Handling
extension BrowserViewController {
func clearCookies(completion: @escaping () -> Void) {
let dataStore = WKWebsiteDataStore.default()
let dataTypes = Set([WKWebsiteDataTypeCookies])
dataStore.fetchDataRecords(ofTypes: dataTypes) { records in
let fireproofedDomains = FireproofingManager.shared.fireproofedDomains
let recordsToDelete = records.filter { record in
!fireproofedDomains.contains(where: { record.displayName.contains($0) })
}
dataStore.removeData(ofTypes: dataTypes, for: recordsToDelete) {
completion()
}
}
}
}
Download Management
Download Delegate
extension BrowserViewController: WKDownloadDelegate {
func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String) async -> URL? {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationURL = documentsURL.appendingPathComponent(suggestedFilename)
// Check if file exists and generate unique name if needed
return FileManager.default.uniqueURL(for: destinationURL)
}
func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) {
// Handle download failure
if let resumeData = resumeData {
// Store resume data for later
DownloadManager.shared.storeResumeData(resumeData, for: download)
}
}
func downloadDidFinish(_ download: WKDownload) {
// Handle successful download
DownloadManager.shared.completeDownload(download)
}
}
Performance Optimization
Memory Management
class TabManager {
private let maxInMemoryTabs = 5
private var tabs: [Tab] = []
func optimizeMemory() {
let activeTabs = tabs.filter { $0.isActive }
let inactiveTabs = tabs.filter { !$0.isActive }
.sorted { $0.lastAccessDate < $1.lastAccessDate }
// Suspend inactive tabs if we have too many in memory
if activeTabs.count + inactiveTabs.count > maxInMemoryTabs {
let tabsToSuspend = inactiveTabs.prefix(inactiveTabs.count - (maxInMemoryTabs - activeTabs.count))
tabsToSuspend.forEach { $0.suspend() }
}
}
}
extension Tab {
func suspend() {
// Take snapshot
webView?.takeSnapshot(with: nil) { [weak self] image, error in
self?.snapshot = image
self?.webView?.removeFromSuperview()
self?.webView = nil
}
}
func resume() {
guard webView == nil else { return }
setupWebView()
if let url = url {
load(url: url)
}
}
}
Security Considerations
Certificate Validation
extension BrowserViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
return (.performDefaultHandling, nil)
}
// Perform certificate pinning for DuckDuckGo domains
if CertificatePinning.shared.shouldPin(host: challenge.protectionSpace.host) {
do {
try CertificatePinning.shared.validate(serverTrust, host: challenge.protectionSpace.host)
let credential = URLCredential(trust: serverTrust)
return (.useCredential, credential)
} catch {
return (.cancelAuthenticationChallenge, nil)
}
}
return (.performDefaultHandling, nil)
}
}