[2026-04-28] Rename apple-browsers rules .mdc → .md for Obsidian compatibility
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
---
|
||||
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/webkit-browser.mdc
|
||||
confidence: 0.9
|
||||
namespace: work
|
||||
last_synced: 2026-04-28
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# WebKit & Browser Development Guidelines
|
||||
|
||||
## WebView Configuration
|
||||
|
||||
### Basic WebView Setup
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
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)
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user