Author SHA1 Message Date
buzzertandClaude Sonnet 5 45b09a13d3 ci: put CI keychain first in codesign search list
TestFlight / testflight (push) Successful in 1m27s
codesign resolves signing identities through the user keychain search
list (first match wins) and ignores --keychain for the lookup. This
runner hosts another project (Sybil-2) whose keychain holds the same
Apple Distribution identity, so if that keychain is locked and appears
earlier in the search list, codesign fails with errSecInternalComponent
no matter how correctly our own keychain is set up. Prepend the fresh
CI keychain to the search list for the build and always delete it
afterward, which restores the original list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 13:16:41 -07:00
buzzert 1632e5fbd4 ITSexempt
TestFlight / testflight (push) Successful in 1m12s
2026-07-06 09:24:36 -07:00
buzzert 18b63cbd19 Attempt to fix stale tabs losing info 2026-07-06 09:24:36 -07:00
buzzert 4371f051aa Switch to using content blockers instead of scheme handlers 2026-07-06 09:24:36 -07:00
buzzert 2c09157448 gitea: try removing temporary keychain
TestFlight / testflight (push) Failing after 1m39s
2026-07-06 09:07:22 -07:00
buzzert 8e26f0a066 Use match signing team for deployment
TestFlight / testflight (push) Failing after 19s
2026-07-06 08:43:11 -07:00
buzzert db947b1827 Set up TestFlight deployment
TestFlight / testflight (push) Failing after 24s
2026-07-06 08:36:11 -07:00
buzzert e53cde7f60 Merge pull request 'codex: add custom search engines' (#1) from feature/custom-search-engines into master
Reviewed-on: #1

Good on iOS, still need to do it on macOS (#2)
2025-09-29 23:09:41 +00:00
buzzert 62e36359b8 finish implementation for iOS 2025-09-29 16:09:00 -07:00
buzzert 265d393cdc codex: add custom search engines 2025-09-28 21:10:31 -07:00
buzzert 797c4c7c52 darkmode css: remove em from reinvert list 2025-09-18 08:52:15 -07:00
buzzert 6cc4694e1e attempt to fix stale tabs 2025-09-06 22:56:51 -07:00
buzzert 334703ec43 Obscure mail spi 2025-05-05 19:05:49 -07:00
buzzert 6301c40c59 Obscure WebKit SPI 2025-05-05 19:01:46 -07:00
buzzert 8fe8426cc2 Delete web process bundle in favor of custom scheme handler 2025-05-05 18:14:46 -07:00
buzzert a2e29abf19 Fixup for distribution 2025-04-11 19:53:02 -07:00
buzzert 2f4b3c64fe Move off of internal SDK, copy webkit spis from opensource 2025-04-11 18:27:59 -07:00
James Magahern 177db26b69 Implements zoom in/out keyboard shortcuts 2024-10-17 13:38:41 -07:00
buzzert 0fd466d86d Adds keyboard shortcuts for changing script policy 2024-08-05 18:09:04 -07:00
buzzert 69aa02e303 Resolves "New Tab" bug after web content process jettison 2024-08-05 17:36:59 -07:00
James Magahern 29057c8024 History menu improvements 2024-07-31 17:53:23 -07:00
buzzert a0056af8b1 Update for iOS 17. 2024-07-29 19:21:51 -07:00
James Magahern 6dccb25b3e TitlebarView: move to drag api 2024-05-10 17:01:14 -07:00
James Magahern eec57f5f31 History UI improvements 2023-09-26 16:23:35 -07:00
buzzert d9fa5adf00 Adds README and screenshot 2023-09-17 20:30:21 -07:00
James Magahern a4c6caeb8e Another attempt to fix empty URL bar 2023-08-10 19:52:59 -07:00
James Magahern 3f5d608908 Even better AI generated icon 2023-04-19 14:39:06 -07:00
James Magahern 99bd3af187 Fix warnings 2023-04-19 14:33:17 -07:00
James Magahern 98dfd85781 Small Settings QOL improvements 2023-04-19 14:30:06 -07:00
James Magahern 6a187b0df3 Update icon to use Midjourney generated one 2023-04-19 11:45:21 -07:00
James Magahern dca5aca1ce UIGraphicsImageContext: both dimensions must be > 0 2023-04-19 11:40:57 -07:00
James Magahern f8d19414d5 Avoid generating zero size bitmaps 2023-04-14 19:37:31 -07:00
James Magahern 748023d330 Adds history menu 2023-01-27 13:46:23 -08:00
buzzert 6f41caff62 HistoryView: Compact UI for phones 2023-01-25 16:13:09 -08:00
buzzert 34ca35ea5a HistoryView: Adds ability to delete history items 2023-01-25 15:04:58 -08:00
James Magahern 53efb5389e Adds history browser view 2023-01-20 17:28:15 -08:00
James Magahern f374f3ebe8 Tweak autocomplete query a bit 2023-01-20 16:20:36 -08:00
James Magahern 836daee5a3 Fix find and replace 2023-01-11 13:20:36 -08:00
buzzert 12e2d82009 Attempt to fix blank URL bar bug 2022-10-11 15:04:59 -07:00
buzzert 6be801420c iOS 16 API updates 2022-10-11 15:04:37 -07:00
James Magahern 75153d1c4a Settings: Catalyst: adjust slightly the header labels 2022-08-22 17:43:04 -07:00
123 changed files with 4624 additions and 822 deletions
+16
View File
@@ -0,0 +1,16 @@
FASTLANE_SKIP_UPDATE_CHECK=1
FASTLANE_HIDE_CHANGELOG=1
FASTLANE_TEAM_ID=DQQH5H6GBD
APP_STORE_CONNECT_KEY_ID=
APP_STORE_CONNECT_ISSUER_ID=
APP_STORE_CONNECT_KEY_CONTENT=
MATCH_GIT_URL=
MATCH_PASSWORD=
MATCH_GIT_BASIC_AUTHORIZATION=
MATCH_GIT_BRANCH=master
ATTRACTOR_PROVISIONING_PROFILE_SPECIFIER=Attractor AppStore CI
ATTRACTOR_VERSION_TAG=
ATTRACTOR_BUILD_NUMBER=
+45
View File
@@ -0,0 +1,45 @@
name: TestFlight
on:
workflow_dispatch:
push:
tags:
- "release/ios/v*"
- "Attractor-*"
jobs:
testflight:
runs-on: xcode
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.1.7"
bundler-cache: true
- name: Upload to TestFlight
env:
HOME: /var/lib/act_runner
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
ATTRACTOR_VERSION_TAG: ${{ github.ref_name }}
FASTLANE_SKIP_UPDATE_CHECK: "1"
FASTLANE_HIDE_CHANGELOG: "1"
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120"
run: |
export PATH="/Users/runner/hostedtoolcache/Ruby/3.1.7/arm64/bin:${PATH}"
ruby --version
bundle exec fastlane ios beta
+5
View File
@@ -0,0 +1,5 @@
.env
.bundle/
build/
fastlane/README.md
fastlane/report.xml
+25
View File
@@ -32,6 +32,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return UISceneConfiguration(name: "Browser", sessionRole: connectingSceneSession.role)
}
private func historyMenu() -> UIMenu {
let historyItems = BrowserHistory.shared.allHistory(limit: 50).map { item in
let title = ((item.title.count > 0) ? item.title : item.url.absoluteString)
.middleTruncatedString(maximumLength: 85)
return UIAction(title: title) { action in
UIApplication.shared.sendAction(#selector(ShortcutResponder.handleOpenURL), to: nil, from: action, for: OpenURLEvent(url: item.url))
}
}
return UIMenu(title: "History", children: [
UIKeyCommand(
modifiers: [ .command ],
input: "y",
title: "Show All History…",
action: #selector(ShortcutResponder.showHistory)
),
UIMenu(options: .displayInline, children: historyItems)
])
}
override var keyCommands: [UIKeyCommand]? {
get { KeyboardShortcuts.allKeyCommands() }
}
@@ -68,6 +90,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Go
builder.insertSibling(UIMenu(title: "Go", children: KeyboardShortcuts.menu(for: .go)), beforeMenu: .view)
// History
builder.insertSibling(historyMenu(), beforeMenu: .view)
// View
builder.replaceChildren(ofMenu: .view) { children in
KeyboardShortcuts.menu(for: .view) + children
+117 -4
View File
@@ -7,9 +7,58 @@
import Foundation
import CoreData
import Combine
class BrowserHistory
{
class ViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
@Published var searchQuery: String = ""
@Published private(set) var historyItems: [HistoryItem] = []
fileprivate let historyController: BrowserHistory
fileprivate var searchQueryObserver: AnyCancellable? = nil
fileprivate var fetchedResultsController: NSFetchedResultsController<HistoryItemEntity> {
didSet { fetchedResultsController.delegate = self; performInitialFetch() }
}
fileprivate init(fetchedResultsController: NSFetchedResultsController<HistoryItemEntity>, historyController: BrowserHistory) {
self.fetchedResultsController = fetchedResultsController
self.historyController = historyController
super.init()
searchQueryObserver = $searchQuery.sink { [unowned self] newValue in
self.fetchedResultsController = historyController.fetchRequestController(forQuery: newValue)
}
}
public func item(forIdentifier identifier: HistoryItem.ID) -> HistoryItem? {
if let object = fetchedResultsController.managedObjectContext.object(with: identifier) as? HistoryItemEntity {
return HistoryItem(entity: object)
}
return nil
}
public func deleteItems(_ items: Set<HistoryItem.ID>) {
items.forEach { identifier in
historyController.deleteItem(withIdentifier: identifier)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
if let objects = controller.fetchedObjects as? [HistoryItemEntity] {
self.historyItems = objects.map { HistoryItem(entity: $0) }
}
}
fileprivate func performInitialFetch() {
try? fetchedResultsController.performFetch()
if let objects = fetchedResultsController.fetchedObjects {
self.historyItems = objects.map { HistoryItem(entity: $0) }
}
}
}
static public let shared = BrowserHistory()
lazy fileprivate var persistentContainer: NSPersistentContainer = {
@@ -35,12 +84,43 @@ class BrowserHistory
let nserror = error as NSError
fatalError("Failed saving persistent entity to store: \(nserror), \(nserror.userInfo)")
}
UIMenuSystem.main.setNeedsRebuild()
}
public func allHistory() -> [HistoryItem] {
let dataContext = persistentContainer.viewContext
public func viewModel(forFilterString filterString: String? = nil, limit: Int = 500) -> ViewModel {
let resultsController = fetchRequestController(forQuery: filterString, limit: limit)
return ViewModel(fetchedResultsController: resultsController, historyController: self)
}
let fetchRequest: NSFetchRequest<HistoryItemEntity> = HistoryItemEntity.fetchRequest()
public func historyItem(forIdentifier: HistoryItem.ID) -> HistoryItem? {
if let entity = try? persistentContainer.viewContext.existingObject(with: forIdentifier) as? HistoryItemEntity {
return HistoryItem(entity: entity)
}
return nil
}
public func deleteItem(withIdentifier identifier: HistoryItem.ID) {
let dataContext = persistentContainer.viewContext
if let object = try? dataContext.existingObject(with: identifier) {
dataContext.delete(object)
try? dataContext.save()
}
}
public func clearAllHistory() {
let dataContext = persistentContainer.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = HistoryItemEntity.fetchRequest()
let request = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do { try dataContext.execute(request) }
catch { print("Error clearing history: \(error.localizedDescription)") }
}
public func allHistory(filteredBy filterString: String? = nil, limit: Int = 500) -> [HistoryItem] {
let dataContext = persistentContainer.viewContext
let fetchRequest = fetchRequest(forStringContaining: filterString, limit: limit)
let entities: [HistoryItemEntity] = (try? dataContext.fetch(fetchRequest)) ?? []
return entities.map { (entity) -> HistoryItem in
@@ -52,7 +132,11 @@ class BrowserHistory
let dataContext = persistentContainer.viewContext
let fetchRequest: NSFetchRequest<HistoryItemEntity> = HistoryItemEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "host CONTAINS %@ OR title contains %@", matching, matching)
fetchRequest.predicate = NSPredicate(format: """
host CONTAINS[cd] %@
OR title CONTAINS[cd] %@
OR url ENDSWITH[cd] %@
""", matching, matching, matching)
fetchRequest.fetchLimit = 200
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "visitCount", ascending: false) ]
@@ -85,6 +169,35 @@ class BrowserHistory
}
}
extension BrowserHistory
{
fileprivate func fetchRequestController(forQuery query: String? = nil, limit: Int = 500) -> NSFetchedResultsController<HistoryItemEntity> {
let fetchRequest = fetchRequest(forStringContaining: query, limit: limit)
return NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: persistentContainer.viewContext,
sectionNameKeyPath: nil, cacheName: nil)
}
fileprivate func fetchRequest(forStringContaining filterString: String? = nil, limit: Int = 500) -> NSFetchRequest<HistoryItemEntity> {
let fetchRequest: NSFetchRequest<HistoryItemEntity> = HistoryItemEntity.fetchRequest()
fetchRequest.fetchLimit = limit
fetchRequest.sortDescriptors = [
// Sort by date
NSSortDescriptor(keyPath: \HistoryItemEntity.lastVisited, ascending: false)
]
if let filterString, filterString.count > 0 {
fetchRequest.predicate = NSPredicate(format: """
host CONTAINS[cd] %@
OR title CONTAINS[cd] %@
OR url CONTAINS[cd] %@
""", filterString, filterString, filterString)
}
return fetchRequest
}
}
extension URL
{
public func topLevelURL() -> URL {
+12 -1
View File
@@ -5,17 +5,28 @@
// Created by James Magahern on 8/14/20.
//
import CoreData
import Foundation
struct HistoryItem: Hashable
struct HistoryItem: Hashable, Identifiable
{
var url: URL
var title: String
var lastVisited: Date
var id: NSManagedObjectID
init(entity: HistoryItemEntity) {
self.url = entity.url ?? URL(string: "about:blank")!
self.lastVisited = entity.lastVisited ?? Date()
self.title = entity.title ?? ""
self.id = entity.objectID
}
// For testing/previews
public init(url: URL, title: String, lastVisited: Date) {
self.url = url
self.title = title
self.lastVisited = lastVisited
self.id = NSManagedObjectID()
}
}
+2 -1
View File
@@ -31,7 +31,8 @@ class ResourcePolicyManager: NSObject, SBRResourceOriginPolicyDataSource
func allowedOriginsForScriptResources() -> Set<String> { allowedOriginSet }
func scriptPolicyTypeByOrigin() -> [String : NSNumber] {
return scriptPolicies.mapValues { NSNumber(integerLiteral: $0.rawValue) }
let types = scriptPolicies.mapValues { NSNumber(integerLiteral: $0.rawValue) }
return types
}
private lazy var scriptPolicies: Dictionary<String, ScriptPolicy.PolicyType> = {
+1
View File
@@ -98,6 +98,7 @@ class BrowserView: UIView
}
}(animationCurve)
toolbarView?.layoutLatch.deactivate()
keyboardLayoutOffset = bounds.height - keyboardEndFrame.minY
UIView.animate(withDuration: animationDuration, delay: 0.0, options: animationOptions, animations: { self.layoutIfNeeded() }, completion: nil)
}
@@ -94,11 +94,11 @@ extension BrowserViewController: ShortcutResponder
}
func goBack(_ sender: Any?) {
tab.webView.goBack()
currentTab.webView.goBack()
}
func goForward(_ sender: Any?) {
tab.webView.goForward()
currentTab.webView.goForward()
}
func createTab(_ sender: Any?) {
@@ -106,24 +106,24 @@ extension BrowserViewController: ShortcutResponder
}
func previousTab(_ sender: Any?) {
if let tabIndex = tabController.tabs.firstIndex(of: self.tab) {
if let tabIndex = tabController.tabs.firstIndex(of: self.currentTab) {
if tabIndex - 1 >= 0 {
self.tab = tabController.tabs[tabIndex - 1]
self.currentTab = tabController.tabs[tabIndex - 1]
}
}
}
func nextTab(_ sender: Any?) {
if let tabIndex = tabController.tabs.firstIndex(of: self.tab) {
if let tabIndex = tabController.tabs.firstIndex(of: self.currentTab) {
if tabIndex + 1 < tabController.tabs.count {
self.tab = tabController.tabs[tabIndex + 1]
self.currentTab = tabController.tabs[tabIndex + 1]
}
}
}
func closeTab(_ sender: Any?) {
if tabController.tabs.count > 1 {
tabController.closeTab(self.tab)
tabController.closeTab(self.currentTab)
} else {
#if targetEnvironment(macCatalyst)
if let originWindowScene = self.view.window?.windowScene {
@@ -144,18 +144,30 @@ extension BrowserViewController: ShortcutResponder
webView.reload()
}
func stop(_ sender: Any?) {
webView.stopLoading()
}
override func increaseSize(_ sender: Any?) {
tab.webView._viewScale += 0.10
currentTab.webView._viewScale += 0.10
}
override func decreaseSize(_ sender: Any?) {
tab.webView._viewScale -= 0.10
currentTab.webView._viewScale -= 0.10
}
func zoomToActualSize(_ sender: Any?) {
currentTab.webView._viewScale = 1.0
}
func showPreferences(_ sender: Any?) {
showSettingsWindow()
}
func showHistory(_ sender: Any?) {
showHistoryWindow()
}
func toggleDarkMode(_ sender: Any?) {
self.darkModeEnabled = !self.darkModeEnabled
}
@@ -163,4 +175,50 @@ extension BrowserViewController: ShortcutResponder
func openInReaderMode(_ sender: Any?) {
showReaderWindow()
}
func handleOpenURL(_ sender: Any?, forEvent event: OpenURLEvent?) {
guard let event else { return }
if currentTab.url == nil {
currentTab.beginLoadingURL(event.url)
} else {
createNewTab(withURL: event.url)
}
}
func raiseScriptPolicyRestriction(_ sender: Any?) {
guard let hostOrigin = currentTab.webView.url?.securityOrigin else { return }
let currentPolicy = policyManager.scriptPolicy(forOrigin: hostOrigin)
policyManager.setScriptPolicyType(currentPolicy.policyType--, forOrigin: hostOrigin)
didChangeScriptPolicy()
}
func lowerScriptPolicyRestriction(_ sender: Any?) {
guard let hostOrigin = currentTab.webView.url?.securityOrigin else { return }
let currentPolicy = policyManager.scriptPolicy(forOrigin: hostOrigin)
policyManager.setScriptPolicyType(currentPolicy.policyType++, forOrigin: hostOrigin)
didChangeScriptPolicy()
}
}
extension ScriptPolicy.PolicyType {
static postfix func ++ (obj: ScriptPolicy.PolicyType) -> ScriptPolicy.PolicyType {
switch obj {
case .alpha: .bravo
case .bravo: .charlie
case .charlie: .delta
case .delta: .echo
default: obj
}
}
static postfix func -- (obj: ScriptPolicy.PolicyType) -> ScriptPolicy.PolicyType {
switch obj {
case .bravo: .alpha
case .charlie: .bravo
case .delta: .charlie
case .echo: .delta
default: obj
}
}
}
@@ -10,35 +10,15 @@ import WebKit
extension BrowserViewController: WKNavigationDelegate, WKUIDelegate
{
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
if let tab = tabController.tab(forWebView: webView) {
// We're alive!
tab.contentProcessTerminated = false
tab.loadError = nil
// Check to make sure we have connected to the web content process
if !tab.bridge.webContentProcessConnected {
// This means we started loading a page but the web content process hasn't loaded, which means
// scripts are not getting blocked.
// If you're ad-hoc signing this, you'll need to disable library validation:
// sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation DisableLibraryValidation -bool YES
DispatchQueue.main.async { [unowned self] in
// Stop loading now
webView.stopLoading()
// Show an alert
let alert = UIAlertController(title: "Web Process Not Loaded",
message: "The web content process never contacted the host application",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
alert.dismiss(animated: true, completion: nil)
}))
present(alert, animated: true, completion: nil)
}
}
// Reset tracking this
tab.allowedScriptOrigins.removeAll()
tab.blockedScriptOrigins.removeAll()
currentTab.allowedScriptOrigins.removeAll()
currentTab.blockedScriptOrigins.removeAll()
updateScriptBlockerButton()
// Blur url bar if applicable
@@ -48,7 +28,7 @@ extension BrowserViewController: WKNavigationDelegate, WKUIDelegate
if let url = webView.url {
// Start requesting favicon
tab.updateFaviconForURL(url)
currentTab.updateFaviconForURL(url)
}
}
@@ -56,7 +36,7 @@ extension BrowserViewController: WKNavigationDelegate, WKUIDelegate
// We got rugged!!!!!! Update the favicon again.
if let url = webView.url {
// Start requesting favicon
tab.updateFaviconForURL(url)
currentTab.updateFaviconForURL(url)
}
}
@@ -90,7 +70,7 @@ extension BrowserViewController: WKNavigationDelegate, WKUIDelegate
return
}
var allowJavaScript = tab.javaScriptEnabled
var allowJavaScript = currentTab.javaScriptEnabled
if !allowJavaScript, let host = navigationAction.request.url?.host {
// Check origin policy
allowJavaScript = policyManager.scriptPolicy(forOrigin: host).allowsEmbeddedJavaScript()
@@ -101,7 +81,7 @@ extension BrowserViewController: WKNavigationDelegate, WKUIDelegate
if let url = navigationAction.request.url,
let redirectedURL = Settings.shared.redirectRule(for: url)
{
tab.beginLoadingURL(redirectedURL)
currentTab.beginLoadingURL(redirectedURL)
decisionHandler(.cancel, preferences)
} else {
decisionHandler(.allow, preferences)
@@ -109,17 +89,21 @@ extension BrowserViewController: WKNavigationDelegate, WKUIDelegate
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
if webView == tab.webView {
tab.loadError = error
if webView == currentTab.webView {
currentTab.loadError = error
}
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
if webView == tab.webView {
tab.loadError = error
if webView == currentTab.webView {
currentTab.loadError = error
}
}
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
tabController.tab(forWebView: webView)?.contentProcessTerminated = true
}
// MARK: WKUIDelegate
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView?
@@ -127,7 +111,7 @@ extension BrowserViewController: WKNavigationDelegate, WKUIDelegate
let newTab = tabController.createNewTab(url: nil, webViewConfiguration: configuration)
newTab.webView.load(navigationAction.request)
self.tab = newTab
self.currentTab = newTab
return newTab.webView
}
+119 -58
View File
@@ -13,8 +13,8 @@ import UniformTypeIdentifiers
class BrowserViewController: UIViewController
{
let browserView = BrowserView()
var tab: Tab { didSet { didChangeTab(tab) } }
var webView: WKWebView { tab.webView }
var currentTab: Tab { didSet { didChangeTab(currentTab) } }
var webView: WKWebView { currentTab.webView }
internal let tabController = TabController()
internal let tabBarViewController: TabBarViewController
@@ -42,9 +42,9 @@ class BrowserViewController: UIViewController
static let longPressWindowButtonToMakeNewTab: Bool = false
internal var darkModeEnabled: Bool {
get { tab.bridge.darkModeEnabled }
get { currentTab.bridge.darkModeEnabled }
set {
tab.bridge.darkModeEnabled = newValue
currentTab.bridge.darkModeEnabled = newValue
toolbarController.darkModeEnabled = newValue
}
}
@@ -60,7 +60,7 @@ class BrowserViewController: UIViewController
internal var changingFocusToAutocompleteController = false
init() {
self.tab = tabController.tabs.first!
self.currentTab = tabController.tabs.first!
self.tabBarViewController = TabBarViewController(tabController: tabController)
super.init(nibName: nil, bundle: nil)
@@ -70,7 +70,7 @@ class BrowserViewController: UIViewController
addChild(findOnPageController)
addChild(tabBarViewController)
didChangeTab(tab)
didChangeTab(currentTab)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
@@ -114,12 +114,12 @@ class BrowserViewController: UIViewController
// Script button
toolbarController.scriptControllerIconView.addAction(UIAction(handler: { [unowned self] action in
let hostOrigin = self.webView.url?.host ?? ""
let loadedScripts = self.tab.allowedScriptOrigins.union(self.tab.blockedScriptOrigins)
let hostOrigin = self.webView.url?.securityOrigin ?? ""
let loadedScripts = currentTab.allowedScriptOrigins.union(currentTab.blockedScriptOrigins)
let scriptViewController = ScriptPolicyViewController(policyManager: self.policyManager,
hostOrigin: hostOrigin,
loadedScripts: loadedScripts,
scriptsAllowedForTab: self.tab.javaScriptEnabled)
scriptsAllowedForTab: currentTab.javaScriptEnabled)
scriptViewController.delegate = self
let navController = UINavigationController(rootViewController: scriptViewController)
@@ -141,7 +141,7 @@ class BrowserViewController: UIViewController
tabPickerController.delegate = self
tabPickerController.title = "Tabs"
tabPickerController.tabBarItem.image = UIImage(systemName: "rectangle.on.rectangle")
tabPickerController.selectedTabIdentifier = self.tab.identifier
tabPickerController.selectedTabIdentifier = currentTab.identifier
tabPickerController.tabObserver = tabController.$tabs
.receive(on: RunLoop.main)
.sink(receiveValue: { (newTabs: [Tab]) in
@@ -207,7 +207,7 @@ class BrowserViewController: UIViewController
// Error button
toolbarController.urlBar.errorButton.addAction(UIAction(handler: { [unowned self] _ in
let alert = UIAlertController(title: "Error", message: self.tab.loadError?.localizedDescription, preferredStyle: .actionSheet)
let alert = UIAlertController(title: "Error", message: currentTab.loadError?.localizedDescription, preferredStyle: .actionSheet)
alert.popoverPresentationController?.sourceView = self.toolbarController.urlBar.errorButton
alert.addAction(UIAlertAction(title: "Reload", style: .destructive, handler: { _ in
@@ -240,7 +240,7 @@ class BrowserViewController: UIViewController
// Font size adjust
toolbarController.urlBar.documentButton.addAction(UIAction(handler: { [unowned self] _ in
let documentControls = DocumentControlViewController(darkModeEnabled: tab.bridge.darkModeEnabled)
let documentControls = DocumentControlViewController(darkModeEnabled: currentTab.bridge.darkModeEnabled)
documentControls.modalPresentationStyle = .popover
documentControls.popoverPresentationController?.permittedArrowDirections = [ .down, .up ]
documentControls.popoverPresentationController?.sourceView = toolbarController.urlBar.documentButton
@@ -250,28 +250,30 @@ class BrowserViewController: UIViewController
numberFormatter.numberStyle = .percent
let label = documentControls.fontSizeAdjustView.labelView
label.text = numberFormatter.string(for: tab.webView._viewScale)
label.text = numberFormatter.string(for: currentTab.webView._viewScale)
// Font size adjust
documentControls.fontSizeAdjustView.decreaseSizeButton.addAction(UIAction(handler: { [unowned self] sender in
self.decreaseSize(sender)
label.text = numberFormatter.string(for: tab.webView._viewScale)
label.text = numberFormatter.string(for: currentTab.webView._viewScale)
}), for: .touchUpInside)
documentControls.fontSizeAdjustView.increaseSizeButton.addAction(UIAction(handler: { [unowned self] sender in
self.increaseSize(sender)
label.text = numberFormatter.string(for: tab.webView._viewScale)
label.text = numberFormatter.string(for: currentTab.webView._viewScale)
}), for: .touchUpInside)
// Find on page
documentControls.findOnPageControlView.addAction(UIAction(handler: { [unowned self] _ in
documentControls.dismiss(animated: true, completion: nil)
documentControls.dismiss(animated: true, completion: { [unowned self] in
// Needs to happen after dismissal, otherwise the find navigator will pick up the document
// controls as the divergent responder/deepest action responder (my bad...).
if FindOnPageViewController.isEnabled() {
browserView.setFindOnPageVisible(true, animated: true)
} else if #available(iOS 16.0, *) {
browserView.webView?._findInteraction.presentFindNavigatorShowingReplace(false)
browserView.webView?.findInteraction?.presentFindNavigator(showingReplace: false)
}
})
}), for: .touchUpInside)
// Navigation controls
@@ -331,6 +333,12 @@ class BrowserViewController: UIViewController
showShareSheetForCurrentURL(fromViewController: documentControls)
}, for: .touchUpInside)
// History
documentControls.historyView.addAction(UIAction { [unowned self] action in
documentControls.dismiss(animated: false, completion: nil)
showHistory(action)
}, for: .touchUpInside)
present(documentControls, animated: true, completion: nil)
}), for: .touchUpInside)
@@ -340,8 +348,8 @@ class BrowserViewController: UIViewController
.sink(receiveValue: { [unowned self] (activeTab: Int) in
if activeTab < tabController.tabs.count {
let tab = tabController.tabs[activeTab]
if self.tab != tab {
self.tab = tab
if currentTab != tab {
currentTab = tab
}
}
@@ -349,6 +357,11 @@ class BrowserViewController: UIViewController
self.updateTabBarVisibility()
})
registerForTraitChanges([UITraitHorizontalSizeClass.self]) { [weak self] (traitEnvironment: Self, previousTraitCollection) in
guard let self else { return }
toolbarController.reconfigureButtons(for: traitCollection.horizontalSizeClass)
}
self.view = browserView
}
@@ -362,7 +375,7 @@ class BrowserViewController: UIViewController
let shareableURL = ShareableURL(
url: url,
title: webView.title ?? url.absoluteString,
favicon: tab.favicon
favicon: currentTab.favicon
)
let activityController = UIActivityViewController(activityItems: [ shareableURL ], applicationActivities: nil)
@@ -436,21 +449,21 @@ class BrowserViewController: UIViewController
}
internal func showReaderWindow() {
tab.bridge.parseDocumentForReaderMode { string in
currentTab.bridge.parseDocumentForReaderMode { [currentTab] string in
DispatchQueue.main.async {
if self.traitCollection.userInterfaceIdiom == .phone {
let readableViewController = ReaderViewController(readableHTMLString: string, baseURL: self.tab.bridge.webView.url)
readableViewController.title = self.tab.bridge.webView.title
readableViewController.darkModeEnabled = self.tab.bridge.darkModeEnabled
let readableViewController = ReaderViewController(readableHTMLString: string, baseURL: currentTab.bridge.webView.url)
readableViewController.title = currentTab.bridge.webView.title
readableViewController.darkModeEnabled = currentTab.bridge.darkModeEnabled
readableViewController.delegate = self
let navigationController = UINavigationController(rootViewController: readableViewController)
self.present(navigationController, animated: true, completion: nil)
} else {
let userActivity = NSUserActivity(activityType: SessionActivityType.ReaderWindow.rawValue)
userActivity.title = self.tab.title
userActivity.title = currentTab.title
userActivity.userInfo = [
ReaderUserActivityKeys.baseURL.rawValue : self.tab.bridge.webView.url ?? NSNull(),
ReaderUserActivityKeys.baseURL.rawValue : currentTab.bridge.webView.url ?? NSNull(),
ReaderUserActivityKeys.htmlString.rawValue : string,
]
let requestOptions = UIWindowScene.ActivationRequestOptions()
@@ -461,8 +474,41 @@ class BrowserViewController: UIViewController
}
}
internal func showHistoryWindow() {
let historyViewController = HistoryBrowserViewController { [unowned self] url in
if currentTab.url == nil {
currentTab.beginLoadingURL(url)
} else {
createNewTab(withURL: url)
}
presentedViewController?.dismiss(animated: true)
}
historyViewController.title = "History"
historyViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction { _ in
// xxx: This is not the SwiftUI-y way to do this.
historyViewController.dismiss(animated: true)
})
historyViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Clear", primaryAction: UIAction { [unowned self] action in
let alertController = UIAlertController(title: "Clear History", message: "Are you sure you want to clear all history?", preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction(title: "Clear", style: .destructive, handler: { [unowned self] _ in
BrowserHistory.shared.clearAllHistory()
presentedViewController?.dismiss(animated: true)
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alertController.popoverPresentationController?.sourceItem = action.presentationSourceItem
presentedViewController?.present(alertController, animated: true)
})
let navigationController = UINavigationController(rootViewController: historyViewController)
present(navigationController, animated: true)
}
internal func updateLoadProgress(forWebView webView: WKWebView) {
if let loadError = tab.loadError {
if let loadError = currentTab.loadError {
toolbarController.urlBar.loadProgress = .error(error: loadError)
} else if webView.estimatedProgress == 1.0 {
toolbarController.urlBar.loadProgress = .complete
@@ -474,6 +520,8 @@ class BrowserViewController: UIViewController
}
internal func updateTitleAndURL(forWebView webView: WKWebView) {
guard let tab = tabController.tab(forWebView: webView) else { return }
if webView == browserView.webView {
browserView.titlebarView.setTitle(webView.title ?? "")
@@ -485,8 +533,7 @@ class BrowserViewController: UIViewController
}
// Figure out which tab this corresponds to
let tab = tabController.tabs.first { $0.webView == webView }
if let tab = tab, let tabIndex = tabController.tabs.firstIndex(of: tab) {
if let tabIndex = tabController.tabs.firstIndex(of: tab) {
tabBarViewController.tabBarView.reloadTab(atIndex: tabIndex)
}
}
@@ -496,9 +543,9 @@ class BrowserViewController: UIViewController
tabController.activeTabIndex = activeIndex
}
tab.delegate = self
currentTab.delegate = self
let webView = tab.webView
let webView = currentTab.webView
webView.allowsBackForwardNavigationGestures = true
webView.navigationDelegate = self
webView.uiDelegate = self
@@ -543,10 +590,23 @@ class BrowserViewController: UIViewController
updateScriptBlockerButton()
// Enforce dark mode setting
tab.bridge.darkModeEnabled = toolbarController.darkModeEnabled
currentTab.bridge.darkModeEnabled = toolbarController.darkModeEnabled
// Blur url bar, if applicable
toolbarController.urlBar.textField.resignFirstResponder()
// If this tab's web content process was jettisoned while in background,
// its current page may appear blank and url be nil. Proactively reload
// when the tab becomes active again to restore state.
if tab.contentProcessTerminated {
if webView.url != nil || tab.url != nil {
webView.reload()
} else if let home = tab.homeURL {
tab.beginLoadingURL(home)
} else {
webView.reload()
}
}
}
override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
@@ -571,18 +631,23 @@ class BrowserViewController: UIViewController
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
becomeFirstResponder()
// If the app resumed and the current tab's web content process was
// terminated while in background, ensure we restore it now.
if currentTab.contentProcessTerminated {
if webView.url != nil || currentTab.url != nil {
webView.reload()
} else if let home = currentTab.homeURL {
currentTab.beginLoadingURL(home)
} else {
webView.reload()
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// Not sure why this doesn't happen automatically...
toolbarController.traitCollectionDidChange(previousTraitCollection)
}
internal func updateScriptBlockerButton() {
var numBlockedScripts: Int = tab.blockedScriptOrigins.count
if tab.url != nil, tab.javaScriptEnabled == false {
var numBlockedScripts: Int = currentTab.blockedScriptOrigins.count
if currentTab.url != nil, currentTab.javaScriptEnabled == false {
// Because the page is blocked too, notify.
numBlockedScripts += 1
}
@@ -593,7 +658,7 @@ class BrowserViewController: UIViewController
}
let iconView = toolbarController.scriptControllerIconView
iconView.shieldsDown = tab.javaScriptEnabled
iconView.shieldsDown = currentTab.javaScriptEnabled
// iconView.setBlockedScriptsNumber(numBlockedScripts)
if let policy = policy {
@@ -608,7 +673,7 @@ class BrowserViewController: UIViewController
let newTab = tabController.createNewTab(url: url)
if !loadInBackground {
self.tab = newTab
currentTab = newTab
if url == nil && traitCollection.userInterfaceIdiom == .mac {
self.toolbarController.urlBar.textField.becomeFirstResponder()
@@ -616,7 +681,7 @@ class BrowserViewController: UIViewController
} else {
// Send this message to get it to load NOW, instead of waiting for it to show up
// in the view hierarchy.
tab.webView.didMoveToWindow()
currentTab.webView.didMoveToWindow()
// Update tab bar now
updateTabBarVisibility()
@@ -637,12 +702,12 @@ extension BrowserViewController: UIPopoverPresentationControllerDelegate
extension BrowserViewController: ScriptPolicyViewControllerDelegate
{
func didChangeScriptPolicy() {
tab.bridge.policyDataSourceDidChange()
currentTab.bridge.policyDataSourceDidChange()
webView.reload()
}
func setScriptsEnabledForTab(_ enabled: Bool) {
tab.javaScriptEnabled = enabled
currentTab.javaScriptEnabled = enabled
toolbarController.scriptControllerIconView.shieldsDown = enabled
}
}
@@ -650,7 +715,7 @@ extension BrowserViewController: ScriptPolicyViewControllerDelegate
extension BrowserViewController: AutocompleteViewControllerDelegate
{
func autocompleteController(_: AutocompleteViewController, didSelectHistoryItem item: HistoryItem) {
tab.beginLoadingURL(item.url)
currentTab.beginLoadingURL(item.url)
autocompleteViewController.view.isHidden = true
}
}
@@ -665,7 +730,7 @@ extension BrowserViewController: TabDelegate
extension BrowserViewController: TabPickerViewControllerDelegate
{
func tabPicker(_ picker: TabPickerViewController, createNewTabWithURL url: URL?) {
self.tab = tabController.createNewTab(url: url)
currentTab = tabController.createNewTab(url: url)
picker.dismiss(animated: true)
}
@@ -684,7 +749,7 @@ extension BrowserViewController: TabPickerViewControllerDelegate
guard let tab else { return }
self.tab = tab
currentTab = tab
picker.dismiss(animated: true, completion: nil)
}
@@ -694,7 +759,7 @@ extension BrowserViewController: TabPickerViewControllerDelegate
tabController.closeTab(tab)
// Dismiss picker if current tab is closed using the picker
if tab == self.tab {
if tab == currentTab {
picker.dismiss(animated: true, completion: nil)
}
}
@@ -728,10 +793,10 @@ extension BrowserViewController: UITextFieldDelegate
}
}
tab.beginLoadingURL(url)
currentTab.beginLoadingURL(url)
} else {
let searchURL = Settings.shared.searchProvider.provider().searchURLWithQuery(text)
tab.beginLoadingURL(searchURL)
let searchURL = Settings.shared.currentSearchProvider().searchURLWithQuery(text)
currentTab.beginLoadingURL(searchURL)
}
textField.resignFirstResponder()
@@ -740,10 +805,6 @@ extension BrowserViewController: UITextFieldDelegate
return false
}
override func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
true
}
func textFieldDidEndEditing(_ textField: UITextField) {
if !changingFocusToAutocompleteController {
autocompleteViewController.view.isHidden = true
@@ -754,7 +815,7 @@ extension BrowserViewController: UITextFieldDelegate
extension BrowserViewController: ReaderViewControllerDelegate
{
func readerViewController(_ reader: ReaderViewController, didRequestNavigationToURL navigationURL: URL) {
tab.beginLoadingURL(navigationURL)
currentTab.beginLoadingURL(navigationURL)
}
}
+7 -9
View File
@@ -28,11 +28,17 @@ class GradientView: UIImageView
self.init(image: nil)
self.direction = direction
self.colors = colors
registerForTraitChanges([UITraitUserInterfaceStyle.self]) { [weak self] (traitEnvironment: Self, previousTraitCollection) in
self?.image = nil
self?.setNeedsLayout()
}
}
private func gradientImage(forSize size: CGSize) -> UIImage? {
var image: UIImage? = nil
guard size.width > .leastNonzeroMagnitude && size.height > .leastNonzeroMagnitude else { return nil }
var image: UIImage? = nil
UIGraphicsBeginImageContext(size)
if let context = UIGraphicsGetCurrentContext() {
let gradientColorsArray = self.colors.map { $0.cgColor }
@@ -69,12 +75,4 @@ class GradientView: UIImageView
generatedImageSize = bounds.size
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// Regenerate image
image = nil
setNeedsLayout()
}
}
+7 -5
View File
@@ -46,6 +46,8 @@ class ReliefButton: UIButton
}
})
self.imageView?.contentMode = .scaleAspectFit
shadowView.alpha = 0.28
shadowView.backgroundColor = UIColor(white: 0.0, alpha: 1.0)
shadowView.isUserInteractionEnabled = false
@@ -67,12 +69,14 @@ class ReliefButton: UIButton
backgroundView.layer.rasterizationScale = UIScreen.main.scale
addSubview(backgroundView)
traitCollectionDidChange(nil)
registerForTraitChanges([UITraitUserInterfaceStyle.self], handler: handleTraitCollectionChange)
pointerStyleProvider = { (button, pointerEffect, pointerShape) -> UIPointerStyle? in
let preview = UITargetedPreview(view: button)
return UIPointerStyle(effect: .lift(preview), shape: pointerShape)
}
handleTraitCollectionChange(self, previousCollection: nil)
}
required init?(coder: NSCoder) {
@@ -97,7 +101,7 @@ class ReliefButton: UIButton
return inverted ? colors.reversed() : colors
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
private func handleTraitCollectionChange(_ environment: UITraitEnvironment, previousCollection: UITraitCollection?) {
setBackgroundInverted(isHighlighted)
backgroundView.layer.borderColor = { traitCollection -> UIColor in
if traitCollection.userInterfaceStyle == .dark {
@@ -105,7 +109,7 @@ class ReliefButton: UIButton
} else {
return .white
}
}(traitCollection).cgColor
} (traitCollection).cgColor
}
internal func setBackgroundInverted(_ inverted: Bool) {
@@ -132,8 +136,6 @@ class ReliefButton: UIButton
}
override func layoutSubviews() {
self.imageView?.contentMode = .scaleAspectFit
super.layoutSubviews()
sendSubviewToBack(backgroundView)
@@ -103,9 +103,16 @@ class DocumentControlItemView: UIControl
separatorView.isHidden = !drawsBottomSeparator
}
override func setTracking(_ tracking: Bool) {
super.setTracking(tracking)
highlightView.isHidden = !tracking
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
super.beginTracking(touch, with: event)
highlightView.isHidden = false
return true
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)
highlightView.isHidden = true
}
public func title(_ title: String) -> Self {
@@ -20,6 +20,7 @@ class DocumentControlViewController: UIViewController
let archiveView = DocumentControlItemView().title("Archive.today") .symbol("shippingbox")
let emailView = DocumentControlItemView().title("Email") .symbol("envelope")
let sharingView = DocumentControlItemView().title("Share") .symbol("square.and.arrow.up")
let historyView = DocumentControlItemView().title("History") .symbol("clock.arrow.circlepath")
let darkModeView = DocumentControlItemView().title("Dark Mode")
var observations: [NSKeyValueObservation] = []
@@ -46,6 +47,7 @@ class DocumentControlViewController: UIViewController
documentControlsView.stackView.addArrangedSubview(darkModeView)
documentControlsView.stackView.addArrangedSubview(readabilityView)
documentControlsView.stackView.addArrangedSubview(archiveView)
documentControlsView.stackView.addArrangedSubview(historyView)
documentControlsView.stackView.addArrangedSubview(settingsView)
@@ -41,7 +41,7 @@ class FindOnPageViewController: UIViewController, _WKFindDelegate
findOnPageView.textField.addAction(UIAction(handler: { [unowned self] _ in
self.findString = findOnPageView.textField.text
webView?._find(self.findString, options: self.findOptions, maxCount: self.maxCount)
webView?._find(self.findString, options: self.findOptions, maxCount: UInt(self.maxCount))
}), for: .editingChanged)
findOnPageView.prevResultButton.addAction(UIAction(handler: { [unowned self] _ in
@@ -82,24 +82,20 @@ class FindOnPageViewController: UIViewController, _WKFindDelegate
@objc
override func findNext(_ sender: Any?) {
webView?._find(self.findString, options: self.findOptions, maxCount: self.maxCount)
webView?._find(self.findString, options: self.findOptions, maxCount: UInt(self.maxCount))
}
@objc
override func findPrevious(_ sender: Any?) {
let options: _WKFindOptions = self.findOptions.union(.backwards)
webView?._find(self.findString, options: options, maxCount: self.maxCount)
webView?._find(self.findString, options: options, maxCount: UInt(self.maxCount))
}
func _webView(_ webView: WKWebView!, didFailToFind string: String!) {
// ??
}
func _webView(_ webView: WKWebView!, didCountMatches matches: UInt, for string: String!) {
// TODO: Update a label
}
func _webView(_ webView: WKWebView!, didFindMatches matches: UInt, for string: String!, withMatch matchIndex: Int) {
internal func _webView(_ webView: WKWebView!, didFindMatches matches: UInt, for string: String!, withMatch matchIndex: Int) {
findOnPageView.nextResultButton.isEnabled = matches > 0
findOnPageView.prevResultButton.isEnabled = matches > 0
}
+12
View File
@@ -0,0 +1,12 @@
//
// Hacks.h
// App
//
// Created by James Magahern on 5/5/25.
//
#import <Foundation/Foundation.h>
extern Class DecodedClass(const char *encodedClassName);
extern SEL DecodedSelector(const char *encodedSelectorName);
extern void SwizzleClassMethod(Class targetClass, SEL originalSelector, IMP replacementIMP);
+31
View File
@@ -0,0 +1,31 @@
//
// Hacks.m
// App
//
// Created by James Magahern on 5/5/25.
//
#import "Hacks.h"
#import <objc/runtime.h>
Class DecodedClass(const char *encodedClassName) {
NSData *data = [NSData dataWithBytesNoCopy:(void *)encodedClassName length:strlen(encodedClassName) freeWhenDone:NO];
data = [[NSData alloc] initWithBase64EncodedData:data options:0];
NSString *str = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
return NSClassFromString(str);
}
SEL DecodedSelector(const char *encodedSelectorName) {
NSData *data = [NSData dataWithBytesNoCopy:(void *)encodedSelectorName length:strlen(encodedSelectorName) freeWhenDone:NO];
data = [[NSData alloc] initWithBase64EncodedData:data options:0];
NSString *str = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
return NSSelectorFromString(str);
}
void SwizzleClassMethod(Class targetClass, SEL originalSelector, IMP replacementIMP)
{
Method originalMethod = class_getClassMethod(targetClass, originalSelector);
method_setImplementation(originalMethod, replacementIMP);
}
+29 -13
View File
@@ -7,27 +7,43 @@
#import <CoreFoundation/CoreFoundation.h>
#import <MessageUI/MessageUI.h>
#import <WebKit/WKWebView.h>
#import <objc/runtime.h>
@interface MFMailComposeViewController (InternalMethods)
+ (BOOL)canSendMailSourceAccountManagement:(NSInteger)sourceAccountManagement;
@end
static void swizzleClassMethod(Class targetClass, SEL originalSelector, IMP replacementIMP)
{
Method originalMethod = class_getClassMethod(targetClass, originalSelector);
method_setImplementation(originalMethod, replacementIMP);
}
#import "Hacks.h"
__attribute__((constructor))
static void swizzleMFMailComposeViewController()
static void swizzleMFMailComposeViewController(void)
{
// Hacks to work around the fact that MFMailComposeViewController is broken in iOS 14+
swizzleClassMethod(MFMailComposeViewController.class, @selector(canSendMail), imp_implementationWithBlock(^BOOL(void) {
SwizzleClassMethod(MFMailComposeViewController.class, @selector(canSendMail), imp_implementationWithBlock(^BOOL(void) {
return YES;
}));
swizzleClassMethod(MFMailComposeViewController.class, @selector(canSendMailSourceAccountManagement:), imp_implementationWithBlock(^BOOL(NSInteger sam) {
SEL canSendMailSourceAccountManagement = DecodedSelector("Y2FuU2VuZE1haWxTb3VyY2VBY2NvdW50TWFuYWdlbWVudDo=");
SwizzleClassMethod(MFMailComposeViewController.class, canSendMailSourceAccountManagement, imp_implementationWithBlock(^BOOL(NSInteger sam) {
return YES;
}));
}
@interface WKWebView (Hacks)
+ (BOOL)orig_handlesURLScheme:(NSString *)scheme;
@end
@implementation WKWebView (Hacks)
+ (BOOL)orig_handlesURLScheme:(NSString *)scheme
{
if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"]) {
return NO;
}
return [self orig_handlesURLScheme:scheme];
}
@end
__attribute__((constructor))
static void swizzleWebKitConfiguration(void)
{
Method origMethod = class_getClassMethod(WKWebView.class, @selector(handlesURLScheme:));
Method replMethod = class_getClassMethod(WKWebView.class, @selector(orig_handlesURLScheme:));
method_exchangeImplementations(origMethod, replMethod);
}
@@ -0,0 +1,20 @@
//
// HistoryBrowserViewController.swift
// App
//
// Created by James Magahern on 1/20/23.
//
import SwiftUI
import UIKit
@MainActor
class HistoryBrowserViewController: UIHostingController<HistoryView> {
public init(onSelectItem: @escaping (URL) -> Void) {
super.init(rootView: HistoryView(viewModel: BrowserHistory.shared.viewModel(limit: 500), onSelectItem: onSelectItem))
}
required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
+78
View File
@@ -0,0 +1,78 @@
//
// HistoryView.swift
// App
//
// Created by James Magahern on 1/20/23.
//
import SwiftUI
import UniformTypeIdentifiers
struct HistoryView: View {
@StateObject public var viewModel: BrowserHistory.ViewModel
@State public var selectedItems = Set<HistoryItem.ID>()
public var onSelectItem: (URL) -> Void
private let dateFormatter = DateFormatter() .. {
$0.locale = Locale.current
$0.dateStyle = .medium
$0.timeStyle = .short
}
@Environment(\.dismiss) private var dismissAction
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
Table(viewModel.historyItems, selection: $selectedItems) {
TableColumn("Title") { item in
VStack(alignment: .leading) {
Text(item.title.count > 0 ? item.title : item.url.absoluteString)
.lineLimit(1)
if horizontalSizeClass == .compact {
Text(item.url.shortString()).font(.caption).lineLimit(1)
Text(dateFormatter.string(from: item.lastVisited))
.font(.caption)
.foregroundColor(.secondary)
}
}
}
TableColumn("URL", value: \.url.absoluteString)
TableColumn("Last Visited") { item in
Text(dateFormatter.string(from: item.lastVisited))
}
}
.contextMenu(forSelectionType: HistoryItem.ID.self, menu: { items in
let historyItems = items.compactMap { viewModel.item(forIdentifier: $0) }
Button("Copy") {
UIPasteboard.general.setItems(historyItems.map { [
UTType.url.identifier : $0.url,
UTType.utf8PlainText.identifier : $0.url.absoluteString,
] })
}
Button("Delete") {
viewModel.deleteItems(items)
}
}, primaryAction: { items in
items.compactMap({ viewModel.item(forIdentifier: $0) }).forEach { item in
onSelectItem(item.url)
}
})
.searchable(text: $viewModel.searchQuery)
}
}
struct HistoryViewPreviewProvider: PreviewProvider {
static var previews: some View {
HistoryView(viewModel: BrowserHistory.shared.viewModel(), onSelectItem: { _ in })
.previewLayout(.fixed(width: 480.0, height: 800.0))
}
}
extension URL {
public func shortString() -> String {
return String(absoluteString.trimmingPrefix(try! Regex("(.+)://")))
}
}
+62
View File
@@ -50,6 +50,28 @@ protocol ShortcutResponder: AnyObject {
@objc
optional func openInReaderMode(_ sender: Any?)
@objc
optional func showHistory(_ sender: Any?)
@objc
optional func handleOpenURL(_ sender: Any?, forEvent event: OpenURLEvent?)
@objc
optional func lowerScriptPolicyRestriction(_ sender: Any?)
@objc
optional func raiseScriptPolicyRestriction(_ sender: Any?)
@objc
optional func zoomToActualSize(_ sender: Any?)
}
public class OpenURLEvent: UIEvent {
let url: URL
public init(url: URL) {
self.url = url
}
}
fileprivate extension Array {
@@ -140,6 +162,22 @@ public class KeyboardShortcuts {
action: #selector(ShortcutResponder.stop)
)
]),
UIMenu(options: .displayInline, children: [
// Raise Script Policy Restriction
UIKeyCommand(
modifiers: [.alternate], input: "x",
title: "Raise Script Policy Restriction",
action: #selector(ShortcutResponder.raiseScriptPolicyRestriction)
),
// Lower Script Policy Restriction
UIKeyCommand(
modifiers: [.alternate], input: "c",
title: "Lower Script Policy Restriction",
action: #selector(ShortcutResponder.lowerScriptPolicyRestriction)
),
])
].removeNulls()
case .go:
@@ -188,6 +226,30 @@ public class KeyboardShortcuts {
case .view:
return [
// Zoom
UIMenu(options: .displayInline, children: [
// Actual Size
UIKeyCommand(
modifiers: .command, input: "0",
title: "Actual Size",
action: #selector(ShortcutResponder.zoomToActualSize)
),
// Increase Zoom
UIKeyCommand(
modifiers: .command, input: "=",
title: "Zoom In",
action: #selector(UIResponder.increaseSize)
),
// Go Forward
UIKeyCommand(
modifiers: .command, input: "-",
title: "Zoom Out",
action: #selector(UIResponder.decreaseSize)
),
]),
// Toggle Dark Mode
UIKeyCommand(
modifiers: [.command], input: "M",
-1
View File
@@ -8,7 +8,6 @@ html, iframe {
-webkit-filter: invert(100%) !important;
}
em,
img,
svg,
form,
@@ -30,7 +30,7 @@ class BrowserSceneDelegate: UIResponder, UIWindowSceneDelegate {
if let urlContext = connectionOptions.urlContexts.first {
let url = urlContext.url
browserViewController.tab.beginLoadingURL(url)
browserViewController.currentTab.beginLoadingURL(url)
}
#if targetEnvironment(macCatalyst)
@@ -59,8 +59,8 @@ class BrowserSceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)
{
for urlContext in URLContexts {
if browserViewController?.tab.url == nil {
browserViewController?.tab.beginLoadingURL(urlContext.url)
if browserViewController?.currentTab.url == nil {
browserViewController?.currentTab.beginLoadingURL(urlContext.url)
} else {
browserViewController?.createNewTab(withURL: urlContext.url)
}
@@ -0,0 +1,74 @@
//
// AddSearchEngineViewController.swift
// App
//
// Created by James Magahern on 9/29/25.
//
import SwiftUI
import UIKit
@MainActor
struct AddSearchEngineView: View
{
@State var model: ViewModel
var body: some View {
Form {
Section {
HStack {
Text("Name")
TextField("Name", text: $model.name)
.multilineTextAlignment(.trailing)
.autocorrectionDisabled()
}
}
Section {
HStack {
Text("URL")
TextField("https://example.com/query=%q", text: $model.url)
.multilineTextAlignment(.trailing)
.autocorrectionDisabled()
.textContentType(.none)
}
} footer: {
Text("URL must contain %q, which will be replaced by the query. ")
}
Section {
Toggle("Set as default", isOn: $model.makeDefault)
}
}
.navigationTitle("Add Search Engine")
}
@MainActor
@Observable
class ViewModel
{
var name: String = ""
var url: String = ""
var makeDefault: Bool = false
}
}
@MainActor
class AddSearchEngineViewController: UIHostingController<AddSearchEngineView>
{
public var viewModel: AddSearchEngineView.ViewModel
init() {
self.viewModel = AddSearchEngineView.ViewModel()
super.init(rootView: AddSearchEngineView(model: viewModel))
}
@MainActor @preconcurrency required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
#Preview {
@Previewable @State var model = AddSearchEngineView.ViewModel()
AddSearchEngineView(model: model)
}
+6 -6
View File
@@ -23,8 +23,8 @@ struct AmberSettingsView: View {
@Environment(\.presentationMode)
@Binding private var presentationMode
@State private var searchProvider = Settings.shared.searchProvider {
didSet { Settings.shared.searchProvider = searchProvider }
@State private var defaultSearchEngineName = Settings.shared.defaultSearchEngineName {
didSet { Settings.shared.defaultSearchEngineName = defaultSearchEngineName }
}
var body: some View {
@@ -35,12 +35,12 @@ struct AmberSettingsView: View {
})
Section(header: Text("Search Provider"), content: {
ForEach(Settings.SearchProviderSetting.allCases, id: \.self, content: { setting in
Button(action: { searchProvider = setting }, label: {
ForEach(Array(Settings.shared.searchEngines.keys).sorted(), id: \.self, content: { name in
Button(action: { defaultSearchEngineName = name }, label: {
HStack {
Text(setting.rawValue)
Text(name)
Spacer()
if searchProvider == setting {
if defaultSearchEngineName == name {
Image(systemName: "checkmark")
}
}
+162 -27
View File
@@ -32,18 +32,45 @@ struct TextFieldContentConfiguration : UIContentConfiguration
{
var text: String = ""
var placeholderText: String? = nil
var textChanged: ((String) -> Void)
var textChanged: ((String) -> Void)? = nil
var pressedReturn: ((UITextField) -> Void)? = nil
var keyboardType: UIKeyboardType = .default
var returnKeyType: UIReturnKeyType = .default
func makeContentView() -> UIView & UIContentView {
let textField = UITextField(frame: .zero)
textField.borderStyle = .roundedRect
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
textField.keyboardType = keyboardType
textField.returnKeyType = returnKeyType
return GenericContentView<UITextField, TextFieldContentConfiguration>(configuration: self, view: textField) { config, textField in
textField.text = config.text
textField.placeholder = config.placeholderText
textField.addAction(UIAction { _ in config.textChanged(textField.text ?? "") }, for: .editingChanged)
if let textChanged = config.textChanged {
textField.addAction(UIAction { _ in textChanged(textField.text ?? "") }, for: .editingChanged)
}
if let pressedReturn = config.pressedReturn {
class ReturnDelegate : NSObject, UITextFieldDelegate {
var pressedReturn: ((UITextField) -> Void)
public init(pressedReturn: @escaping ((UITextField) -> Void)) {
self.pressedReturn = pressedReturn
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
pressedReturn(textField)
return false
}
}
let delegate = ReturnDelegate(pressedReturn: pressedReturn)
objc_setAssociatedObject(textField, "returnDelegate", delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
textField.delegate = delegate
}
}
}
@@ -90,9 +117,10 @@ class GeneralSettingsViewController: UIViewController
let dataSource: UICollectionViewDiffableDataSource<Section, Item>
let collectionView: UICollectionView
let viewModel: ViewModel
static func createLayout(forIdiom idiom: UIUserInterfaceIdiom) -> UICollectionViewLayout {
#if targetEnvironment(macCatalyst)
#if targetEnvironment(macCatalyst)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
@@ -111,7 +139,7 @@ class GeneralSettingsViewController: UIViewController
layoutSize: headerFooterSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .topLeading,
absoluteOffset: CGPoint(x: insets.leading, y: insets.top)
absoluteOffset: CGPoint(x: insets.leading, y: insets.top + 5.0)
)
sectionHeader.extendsBoundary = false
sectionHeader.contentInsets = insets
@@ -124,11 +152,11 @@ class GeneralSettingsViewController: UIViewController
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
#else
#else
var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
listConfiguration.headerMode = .supplementary
return UICollectionViewCompositionalLayout.list(using: listConfiguration)
#endif
#endif
}
static func sectionHeaderConfiguration(forIdiom idiom: UIUserInterfaceIdiom, sectionName: String) -> UIContentConfiguration {
@@ -139,33 +167,41 @@ class GeneralSettingsViewController: UIViewController
textAlignment: .right
)
} else {
var config = UIListContentConfiguration.plainHeader()
var config = UIListContentConfiguration.header()
config.text = sectionName
return config
}
}
#if targetEnvironment(macCatalyst)
#if targetEnvironment(macCatalyst)
static let staticIdiom = UIUserInterfaceIdiom.mac
#else
#else
static let staticIdiom = UIUserInterfaceIdiom.pad
#endif
#endif
init() {
let actionHandler = { (action: UIAction) in
let providerString = action.title
let provider = Settings.SearchProviderSetting(rawValue: providerString)!
Settings.shared.searchProvider = provider
let engineName = action.title
Settings.shared.defaultSearchEngineName = engineName
}
let viewModel = ViewModel()
let itemCellRegistry = UICollectionView.CellRegistration<UICollectionViewCell, Item> { cell, indexPath, identifier in
if identifier == Self.SearchProviderPopupItem {
let menu = UIMenu(children: Settings.SearchProviderSetting.allCases.map { provider in
let action = UIAction(title: provider.rawValue, handler: actionHandler)
action.state = Settings.shared.searchProvider == provider ? .on : .off
let names = Settings.shared.searchEngines.keys.sorted()
var engineMenuItems: [UIMenuElement] = names.map { name in
let action = UIAction(title: name, handler: actionHandler)
action.state = (Settings.shared.defaultSearchEngineName == name) ? .on : .off
return action
})
}
engineMenuItems.append(UIMenu(options: .displayInline, children: [
UIAction(title: "Manage Search Engines…", handler: { _ in
viewModel.onManageSearchEngines()
})
]))
let menu = UIMenu(children: engineMenuItems)
cell.contentConfiguration = ButtonContentConfiguration(menu: menu)
} else if identifier == Self.SyncServerItem {
cell.contentConfiguration = TextFieldContentConfiguration(
@@ -173,12 +209,15 @@ class GeneralSettingsViewController: UIViewController
placeholderText: "https://sync.server.com",
textChanged: { newString in
Settings.shared.syncServer = newString
}
},
pressedReturn: { $0.resignFirstResponder() },
keyboardType: .URL,
returnKeyType: .done
)
}
#if !targetEnvironment(macCatalyst)
cell.backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
cell.backgroundConfiguration = UIBackgroundConfiguration.listCell()
#endif
}
@@ -201,10 +240,66 @@ class GeneralSettingsViewController: UIViewController
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderRegistry, for: indexPath)
}
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
collectionView.delegate = self
tabBarItem.title = "General"
tabBarItem.image = UIImage(systemName: "gear")
let resetPopup = { [weak self] in
guard let self else { return }
var snapshot = dataSource.snapshot()
snapshot.reloadItems([ Self.SearchProviderPopupItem ])
dataSource.apply(snapshot, animatingDifferences: false)
}
viewModel.onManageSearchEngines = { [weak self] in
guard let self else { return }
// Reset menu item (to show currently selected search engine)
resetPopup()
let viewController = ManageSearchEnginesViewController()
viewController.title = "Manage Search Engines"
viewController.model.onAddSearchEngine = { [weak self] in
guard let self else { return }
let addSearchEngineViewController = AddSearchEngineViewController()
let navController = UINavigationController(rootViewController: addSearchEngineViewController)
// Cancel
addSearchEngineViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .cancel, primaryAction: UIAction { _ in
navController.dismiss(animated: true)
})
// Done
addSearchEngineViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction { [weak self] _ in
self?.saveCustomSearchEngine(model: addSearchEngineViewController.viewModel)
navController.dismiss(animated: true)
resetPopup()
viewController.model.reloadConfiguredEngines()
})
self.present(navController, animated: true)
}
viewController.model.onChangeEngines = {
resetPopup()
}
#if targetEnvironment(macCatalyst)
let alertController = UIAlertController(title: "Not yet implemented for macOS.", message: nil, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in alertController.dismiss(animated: true) }))
present(alertController, animated: true)
#else
navigationController?.pushViewController(viewController, animated: true)
#endif
}
}
required init?(coder: NSCoder) {
@@ -217,14 +312,54 @@ class GeneralSettingsViewController: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
var snapshot = dataSource.snapshot()
snapshot.appendSections(Section.allCases)
// iOS
// snapshot.appendItems(Settings.SearchProviderSetting.allCases.map { $0.rawValue }, toSection: .searchEngine)
snapshot.appendItems([ Self.SearchProviderPopupItem ], toSection: .searchEngine)
snapshot.appendItems([ Self.SyncServerItem ], toSection: .syncServer)
dataSource.apply(snapshot, animatingDifferences: false)
applySnapshot(animatingDifferences: false)
}
// MARK: - Types
@MainActor
@Observable
class ViewModel
{
var onManageSearchEngines: () -> Void = {}
}
}
extension GeneralSettingsViewController : UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
false
}
}
private extension GeneralSettingsViewController {
func applySnapshot(animatingDifferences: Bool) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(Section.allCases)
snapshot.appendItems([ Self.SearchProviderPopupItem ], toSection: .searchEngine)
snapshot.appendItems([ Self.SyncServerItem ], toSection: .syncServer)
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
func saveCustomSearchEngine(model: AddSearchEngineView.ViewModel) {
let name = model.name.trimmingCharacters(in: .whitespacesAndNewlines)
let url = model.url.trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty, !url.isEmpty else { return }
// Require placeholder
guard url.contains("%q") || url.contains("%s") else { return }
// Save
var engines = Settings.shared.searchEngines
engines[name] = url
Settings.shared.searchEngines = engines
if model.makeDefault {
Settings.shared.defaultSearchEngineName = name
}
// Reset inputs and refresh UI
var snapshot = dataSource.snapshot()
snapshot.reloadItems([ Self.SearchProviderPopupItem ])
dataSource.apply(snapshot, animatingDifferences: false)
}
}
@@ -0,0 +1,139 @@
//
// ManageSearchEnginesViewController.swift
// App
//
// Created by James Magahern on 9/29/25.
//
import SwiftUI
import UIKit
@MainActor
struct ManageSearchEnginesView: View
{
@State var model: ViewModel
var body: some View {
Form {
Section {
List($model.configuredEngines, editActions: .delete) { row in
HStack {
Image(uiImage: .checkmark)
.opacity(row.wrappedValue.isDefault ? 1.0 : 0.0)
VStack(alignment: .leading) {
Text(row.wrappedValue.name)
.font(.body)
.bold()
Text(row.wrappedValue.url)
.font(.caption)
}
}
.id(row.id)
.swipeActions(edge: .leading) {
Button { model.makeDefault(row.id) } label: {
Label("Make Default", systemImage: "checkmark")
}
.tint(.blue)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) { model.deleteEngine(row.id) } label: {
Label("Delete", systemImage: "trash")
}
.disabled(model.configuredEngines.count <= 1)
}
}
}
Section {
Button("Add Search Engine…") {
model.onAddSearchEngine()
}
}
}
}
// MARK: - Types
@MainActor
@Observable
class ViewModel
{
var configuredEngines: [ConfiguredEngine] = []
var onAddSearchEngine: () -> Void = {}
var onChangeEngines: () -> Void = {}
init() {
reloadConfiguredEngines()
}
func reloadConfiguredEngines() {
let defaultEngine = Settings.shared.defaultSearchEngineName
self.configuredEngines = Settings.shared.searchEngines.map { val in
ConfiguredEngine(
name: val.key,
url: val.value,
isDefault: val.key == defaultEngine
)
}
}
func deleteEngine(_ id: ConfiguredEngine.ID) {
let engine = configuredEngines.first { $0.url == id }!
var engines = Settings.shared.searchEngines
engines.removeValue(forKey: engine.name)
Settings.shared.searchEngines = engines
if engine.isDefault {
// Pick another default
if let firstEngine = engines.first {
Settings.shared.defaultSearchEngineName = firstEngine.key
}
}
reloadConfiguredEngines()
onChangeEngines()
}
func makeDefault(_ id: ConfiguredEngine.ID) {
let engineName = configuredEngines.first { $0.url == id }!.name
Settings.shared.defaultSearchEngineName = engineName
reloadConfiguredEngines()
onChangeEngines()
}
}
struct ConfiguredEngine: Identifiable
{
let name: String
let url: String
let isDefault: Bool
var id: String { url }
}
}
@MainActor
class ManageSearchEnginesViewController: UIHostingController<ManageSearchEnginesView>
{
let model: ManageSearchEnginesView.ViewModel
init() {
self.model = ManageSearchEnginesView.ViewModel()
super.init(rootView: ManageSearchEnginesView(model: model))
}
required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
#Preview {
@Previewable @State var model = ManageSearchEnginesView.ViewModel()
ManageSearchEnginesView(model: model)
}
@@ -229,13 +229,19 @@ class CreateRedirectRuleViewController: UICollectionViewController
sectionHeaderImageView.contentMode = .scaleAspectFit
fromHostField.placeholder = "https://fromhostname.com"
fromHostField.placeholder = "fromhostname.com"
fromHostField.autocorrectionType = .no
fromHostField.autocapitalizationType = .none
fromHostField.keyboardType = .URL
fromHostField.returnKeyType = .next
fromHostField.delegate = self
toHostField.placeholder = "https://tohostname.com"
toHostField.placeholder = "tohostname.com"
toHostField.autocorrectionType = .no
toHostField.autocapitalizationType = .none
toHostField.keyboardType = .URL
toHostField.returnKeyType = .done
toHostField.delegate = self
}
required init?(coder: NSCoder) {
@@ -256,3 +262,15 @@ class CreateRedirectRuleViewController: UICollectionViewController
}
}
extension CreateRedirectRuleViewController : UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == fromHostField {
toHostField.becomeFirstResponder()
} else if textField == toHostField {
toHostField.resignFirstResponder()
}
return false
}
}
+18 -16
View File
@@ -75,24 +75,26 @@ class Settings
{
static let shared = Settings()
public enum SearchProviderSetting: String, CaseIterable {
case google = "Google"
case duckduckgo = "DuckDuckGo"
case searxnor = "Searx.nor"
case whoogle = "Whoogle.nor"
// Map of search engine name -> URL template containing %q or %s placeholder
// Defaults preserve the previous built-in engines
@SettingProperty(key: "searchEngines")
public var searchEngines: [String: String] = [
"Google": "https://google.com/search?q=%q&gbv=1",
"DuckDuckGo": "https://html.duckduckgo.com/html/?q=%q",
]
func provider() -> SearchProvider {
switch self {
case .google: return SearchProvider.google
case .duckduckgo: return SearchProvider.duckduckgo
case .searxnor: return SearchProvider.searxnor
case .whoogle: return SearchProvider.whoogle
}
}
}
// Name of the default search engine from `searchEngines`
@SettingProperty(key: "defaultSearchEngine")
public var defaultSearchEngineName: String = "Searx.nor"
@SettingProperty(key: "searchProvider")
public var searchProvider: SearchProviderSetting = .searxnor
// Convenience to build a SearchProvider from current default
func currentSearchProvider() -> SearchProvider {
if let template = searchEngines[defaultSearchEngineName] {
return SearchProvider.fromTemplate(template)
}
// Fallback to Google if something goes wrong
return SearchProvider.fromTemplate("https://google.com/search?q=%q&gbv=1")
}
@SettingProperty(key: "redirectRules")
public var redirectRules: [String: String] = [:]
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -1,110 +1,114 @@
{
"images" : [
{
"filename" : "icon-Notification@2x.png",
"idiom" : "iphone",
"filename" : "AppIcon-20.0x20.0@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "icon-Notification@3x.png",
"idiom" : "iphone",
"filename" : "AppIcon-20.0x20.0@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "icon-Settings@2x.png",
"idiom" : "iphone",
"filename" : "AppIcon-29.0x29.0@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "icon-Settings@3x.png",
"idiom" : "iphone",
"filename" : "AppIcon-29.0x29.0@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "icon-Spotlight-40@2x.png",
"idiom" : "iphone",
"filename" : "AppIcon-38.0x38.0@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "38x38"
},
{
"filename" : "AppIcon-38.0x38.0@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "38x38"
},
{
"filename" : "AppIcon-40.0x40.0@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon-Spotlight-40@3x.png",
"idiom" : "iphone",
"filename" : "AppIcon-40.0x40.0@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "icon-60@2x.png",
"idiom" : "iphone",
"filename" : "AppIcon-60.0x60.0@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "icon-60@3x.png",
"idiom" : "iphone",
"filename" : "AppIcon-60.0x60.0@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "icon-Notification~ipad.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "icon-Notification@2x ~ipad.png",
"idiom" : "ipad",
"filename" : "AppIcon-64.0x64.0@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "20x20"
"size" : "64x64"
},
{
"filename" : "icon-Settings~ipad.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
"filename" : "AppIcon-64.0x64.0@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "64x64"
},
{
"filename" : "icon-Settings@2x~ipad.png",
"idiom" : "ipad",
"filename" : "AppIcon-68.0x68.0@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "29x29"
"size" : "68x68"
},
{
"filename" : "icon-Spotlight-40~ipad.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "icon-Spotlight-40@2x~ipad.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon-76~ipad.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "icon-76@2x~ipad.png",
"idiom" : "ipad",
"filename" : "AppIcon-76.0x76.0@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "icon-83.5@2x~ipad.png",
"idiom" : "ipad",
"filename" : "AppIcon-83.5x83.5@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "iconios~marketing-1.png",
"idiom" : "ios-marketing",
"filename" : "AppIcon-1024.0x1024.0@1x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "1x",
"size" : "1024x1024"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

+4
View File
@@ -34,6 +34,10 @@
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
@@ -5,8 +5,9 @@
#import "SBRProcessBundleBridge.h"
#import "SBRScriptPolicy.h"
#import "WebKitDefines.h"
// SPI
#import <UIKit/UITextField_Private.h>
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/_WKFindDelegate.h>
+2 -4
View File
@@ -2,13 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.web-browser</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.developer.web-browser</key>
<true/>
<key>com.apple.developer.device-information.user-assigned-device-name</key>
<true/>
</dict>
</plist>
+19
View File
@@ -0,0 +1,19 @@
//
// WebKitDefines.h
// SBrowser
//
// Created by James Magahern on 4/11/25.
//
#ifndef WebKitDefines_h
#define WebKitDefines_h
#define WK_API_AVAILABLE(...)
#define WK_API_UNAVAILABLE(...)
#define WK_CLASS_AVAILABLE(...) __attribute__((visibility("default"))) WK_API_AVAILABLE(__VA_ARGS__)
#define WK_API_DEPRECATED(_message, ...) __attribute__((deprecated(_message)))
#define WK_API_DEPRECATED_WITH_REPLACEMENT(_replacement, ...) __attribute__((deprecated("use " #_replacement)))
#define WK_CLASS_DEPRECATED_WITH_REPLACEMENT(_replacement, ...) __attribute__((visibility("default"))) __attribute__((deprecated("use " #_replacement)))
#endif /* WebKitDefines_h */
+12 -12
View File
@@ -19,12 +19,7 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
public var tabInfo: TabInfo {
get {
TabInfo(
title: loadedWebView?.title,
urlString: loadedWebView?.url?.absoluteString ?? self.homeURL?.absoluteString,
faviconData: self.favicon?.pngData(),
identifier: self.identifier
)
return _tabInfo
}
}
@@ -43,17 +38,15 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
}
public var policyManager: ResourcePolicyManager
// Persisted snapshot of visible tab metadata; do not recompute on read.
var _tabInfo: TabInfo = TabInfo()
private var loadedWebView: WKWebView? = nil
public var title: String? { get { tabInfo.title } }
public var url: URL? {
get {
if let urlString = tabInfo.urlString {
return URL(string: urlString)
}
if let urlString = tabInfo.urlString { return URL(string: urlString) }
return nil
}
}
public var javaScriptEnabled: Bool = false {
didSet { bridge.allowAllScripts = javaScriptEnabled }
@@ -82,6 +75,7 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
@Published public var favicon: UIImage?
public var loadError: Error?
public var contentProcessTerminated: Bool = false
private var faviconHost: String?
private var faviconRequest: AnyCancellable?
@@ -111,6 +105,10 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
super.init()
bridge.delegate = self
// Initialize snapshot metadata
_tabInfo.identifier = self.identifier
if let url { _tabInfo.urlString = url.absoluteString }
}
deinit {
@@ -118,6 +116,8 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
}
func beginLoadingURL(_ url: URL) {
// Update snapshot immediately so UI keeps URL even if process jettisons.
_tabInfo.urlString = url.absoluteString
let request = URLRequest(url: url)
webView.load(request)
}
+19 -3
View File
@@ -33,6 +33,10 @@ class TabController
tabs.first { $0.identifier == identifier }
}
func tab(forWebView webView: WKWebView) -> Tab? {
tabs.first { $0.webView == webView }
}
func createNewTab(url: URL?) -> Tab {
return self.createNewTab(url: url, webViewConfiguration: nil)
}
@@ -45,16 +49,28 @@ class TabController
tabs.append(tab)
}
// Title observation
// Title observation: update snapshot and notify delegate.
tab.titleObservation = tab.webView.observe(\.title, changeHandler: { [weak tab, weak self] (webView, change) in
if let tab = tab, let self = self, let delegate = self.controllerDelegate {
guard let tab = tab else { return }
if let newTitle = webView.title, !newTitle.isEmpty {
tab._tabInfo.title = newTitle
}
if let self = self, let delegate = self.controllerDelegate {
delegate.tabController(self, didUpdateTitle: webView.title ?? "", forTab: tab)
}
})
// URL observation: persist the latest URL in the snapshot.
tab.urlObservation = tab.webView.observe(\.url, changeHandler: { [weak tab] (webView, change) in
guard let tab = tab else { return }
tab._tabInfo.urlString = webView.url?.absoluteString ?? tab._tabInfo.urlString
})
// Favicon Observation
tab.faviconObservation = tab.$favicon.receive(on: RunLoop.main).sink { [weak tab, weak self] val in
if let tab = tab, let self = self, let delegate = self.controllerDelegate {
guard let tab = tab else { return }
tab._tabInfo.faviconData = val?.pngData()
if let self = self, let delegate = self.controllerDelegate {
delegate.tabController(self, didUpdateFavicon: val, forTab: tab)
}
}
+6 -6
View File
@@ -6,7 +6,6 @@
//
import UIKit
import QuartzCore_Private
class SecurityIndicatorView: UIView
{
@@ -62,10 +61,6 @@ class TitlebarView: UIView
private let separatorView = UIView(frame: .zero)
private let securityIndicatorView = SecurityIndicatorView()
override var _sceneDraggingBehaviorOnPan: _UIViewSceneDraggingBehaviorOnPan {
_UIViewSceneDraggingBehaviorOnPan.dragScene
}
convenience init() {
self.init(frame: .zero)
addSubview(backgroundView)
@@ -74,7 +69,7 @@ class TitlebarView: UIView
addSubview(separatorView)
separatorView.backgroundColor = UIColor(white: 1.0, alpha: 0.20)
separatorView.layer.compositingFilter = kCAFilterPlusL
separatorView.layer.compositingFilter = "kCAFilterPlusL"
titleLabelView.textColor = .white
titleLabelView.layer.shadowColor = UIColor.black.cgColor
@@ -88,6 +83,11 @@ class TitlebarView: UIView
securityIndicatorView.label.font = UIFont.systemFont(ofSize: 10.0)
backgroundView.alpha = 0.98
// Make draggable
if #available(iOS 17.0, *) {
addInteraction(UIWindowSceneDragInteraction())
}
}
func setTitle(_ title: String) {
+15 -2
View File
@@ -9,9 +9,18 @@ import UIKit
class ToolbarView: UIView
{
var urlBar: URLBar? { didSet { containerView.addSubview(urlBar!) } }
var urlBar: URLBar? {
didSet {
guard let urlBar else { return }
containerView.addSubview(urlBar)
var cancelButtonVisible: Bool = false { didSet { layoutSubviews() } }
urlBar.textField.addAction(.init(handler: { [unowned self] _ in
layoutLatch.activate()
}), for: [ .editingDidBegin, .editingDidEnd ])
}
}
var cancelButtonVisible: Bool = false { didSet { setNeedsLayout() } }
let containerView = UIView(frame: .zero)
let backgroundView = GradientView(direction: .vertical, colors: [
@@ -23,6 +32,9 @@ class ToolbarView: UIView
let leadingButtonsView = ToolbarButtonContainerView(frame: .zero)
let trailingButtonsView = ToolbarButtonContainerView(frame: .zero)
// Something I'm sure I'll regret: to ensure animation with the keyboard, latch layout until we get the right signal.
lazy var layoutLatch = LayoutLatch(self)
convenience init()
{
self.init(frame: .zero)
@@ -54,6 +66,7 @@ class ToolbarView: UIView
override func layoutSubviews()
{
guard !layoutLatch.latched else { return }
super.layoutSubviews()
let shadowPath = UIBezierPath(rect: bounds)
@@ -66,10 +66,13 @@ class ToolbarViewController: UIViewController
newTabButton.setImage(UIImage(systemName: "plus"), for: .normal)
let toolbarAnimationDuration: TimeInterval = 0.3
urlBar.textField.addAction(.init(handler: { [traitCollection, toolbarView, urlBar] _ in
if traitCollection.horizontalSizeClass == .compact {
UIView.animate(withDuration: toolbarAnimationDuration) {
toolbarView.cancelButtonVisible = urlBar.textField.isFirstResponder
urlBar.textField.addAction(.init(handler: { [weak self] _ in
guard let self, let parent else { return }
// xxx: Not sure why I need to refer to parent here. This view controller isn't inheriting anything.
if parent.traitCollection.horizontalSizeClass == .compact {
UIView.animate(withDuration: toolbarAnimationDuration) { [unowned self] in
self.toolbarView.cancelButtonVisible = self.urlBar.textField.isFirstResponder
}
}
}), for: [ .editingDidBegin, .editingDidEnd ])
@@ -78,12 +81,10 @@ class ToolbarViewController: UIViewController
urlBar.textField.resignFirstResponder()
}), for: .touchUpInside)
traitCollectionDidChange(nil)
reconfigureButtons(for: traitCollection.horizontalSizeClass)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
public func reconfigureButtons(for horizontalSizeClass: UIUserInterfaceSizeClass) {
toolbarView.leadingButtonsView.removeAllButtonViews()
toolbarView.trailingButtonsView.removeAllButtonViews()
@@ -92,7 +93,7 @@ class ToolbarViewController: UIViewController
}
// Setup toolbar based on trait collection
if traitCollection.horizontalSizeClass == .compact {
if horizontalSizeClass == .compact {
toolbarView.trailingButtonsView.addButtonView(scriptControllerIconView)
toolbarView.trailingButtonsView.addButtonView(newTabButton)
toolbarView.trailingButtonsView.addButtonView(windowButton)
+20 -12
View File
@@ -153,20 +153,12 @@ class URLBar: ReliefButton
textField.font = .systemFont(ofSize: 13.0)
textField.clearButtonMode = .never
textField.placeholder = "URL or search term"
textField.addAction(UIAction(handler: { [unowned self] _ in
// Mask view visibility is affected by editing state.
self.layoutSubviews()
}), for: [ .editingDidBegin, .editingDidEnd ])
textField.keyCommands = [
UIKeyCommand(action: #selector(Self.downKeyPressed), input: UIKeyCommand.inputDownArrow)
.prioritizeOverSystem()
]
addSubview(textField)
textField.addAction(.init(handler: { [textField, refreshButton ] _ in
refreshButton.isHidden = textField.isFirstResponder
}), for: [ .editingDidBegin, .editingDidEnd ])
refreshButton.tintColor = .secondaryLabel
refreshButton.setImage(refreshImage, for: .normal)
refreshButton.isPointerInteractionEnabled = true
@@ -189,8 +181,9 @@ class URLBar: ReliefButton
documentSeparatorView.backgroundColor = .secondarySystemFill
addSubview(documentSeparatorView)
controlsView.autoresizingMask = []
controlsView.clearButton.addAction(.init(handler: { [textField] _ in
textField.clearText()
textField.text = ""
}), for: .primaryActionTriggered)
controlsView.autocorrectButton.addAction(.init(handler: { [unowned self] _ in
@@ -281,8 +274,9 @@ class URLBar: ReliefButton
}
private func fadeBackgroundImageForSize(_ size: CGSize, cutoffLocation: CGFloat) -> UIImage? {
var image: UIImage? = nil
guard size.width > .leastNonzeroMagnitude && size.height > .leastNonzeroMagnitude else { return nil }
var image: UIImage? = nil
UIGraphicsBeginImageContext(CGSize(width: size.width, height: 1.0))
if let context = UIGraphicsGetCurrentContext() {
let gradientColorsArray = [
@@ -292,6 +286,7 @@ class URLBar: ReliefButton
UIColor(white: 1.0, alpha: 0.08).cgColor
]
let cutoffLocation = CGFloat.minimum(0.9, cutoffLocation)
let locations: [CGFloat] = [
0.0, cutoffLocation, cutoffLocation + 0.10, 1.0
]
@@ -343,6 +338,9 @@ class URLBar: ReliefButton
)
documentSeparatorView.frame = documentSeparatorView.frame.insetBy(dx: 0.0, dy: 3.0)
// Text field controls
controlsView.frame = CGRect(origin: controlsView.frame.origin, size: controlsView.sizeThatFits(bounds.size))
// Text field
let textFieldPadding: CGFloat = 6.0
let textFieldOrigin = CGPoint(x: documentButton.frame.maxX + textFieldPadding, y: 0.0)
@@ -360,8 +358,18 @@ class URLBar: ReliefButton
let refreshButtonSize = CGSize(width: textField.frame.height, height: textField.frame.height)
refreshButton.frame = CGRect(origin: CGPoint(x: bounds.width - refreshButtonSize.width, y: 0), size: refreshButtonSize)
// Refresh vs. controls view visibility
let isFocused = textField.isFirstResponder
if isFocused {
controlsView.alpha = 1.0
refreshButton.alpha = 0.0
} else {
controlsView.alpha = 0.0
refreshButton.alpha = 1.0
}
// Error button
if case .error(error: _) = loadProgress, !textField.isFirstResponder {
if case .error(error: _) = loadProgress, !isFocused {
errorButton.isHidden = false
errorButton.sizeToFit()
errorButton.frame = CGRect(
@@ -379,7 +387,7 @@ class URLBar: ReliefButton
// Fade mask
fadeMaskView.frame = textField.bounds
fadeMaskView.image = fadeBackgroundImageForSize(fadeMaskView.frame.size, cutoffLocation: fadeCutoffLocation)
if !textField.isFirstResponder {
if !isFocused {
textField.mask = fadeMaskView
} else {
textField.mask = nil
+2 -2
View File
@@ -33,14 +33,14 @@ extension CGRect
public func centeredY(inRect: CGRect) -> CGRect {
var rect = self
rect.origin.y = CGRound(inRect.origin.y + (inRect.height - rect.height) / 2.0)
rect.origin.y = (inRect.origin.y + (inRect.height - rect.height) / 2.0).rounded()
return rect
}
public func centeredX(inRect: CGRect) -> CGRect {
var rect = self
rect.origin.x = CGRound(inRect.origin.x + (inRect.width - rect.width) / 2.0)
rect.origin.x = (inRect.origin.x + (inRect.width - rect.width) / 2.0).rounded()
return rect
}
+30
View File
@@ -0,0 +1,30 @@
//
// LayoutLatch.swift
// App
//
// Created by James Magahern on 8/10/23.
//
import Foundation
class LayoutLatch {
public weak var view: UIView?
public private(set) var latched: Bool = false
init(_ view: UIView) {
self.view = view
}
public func activate() {
latched = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in
self?.deactivate()
}
}
public func deactivate() {
latched = false
view?.setNeedsLayout()
}
}
+33
View File
@@ -0,0 +1,33 @@
//
// StringUtilities.swift
// App
//
// Created by James Magahern on 7/31/24.
//
import Foundation
extension String {
internal func middleTruncatedString(maximumLength: Int) -> String {
if maximumLength > self.count {
return self
}
// Compute replacement range
let pivot = self.count / 2
let truncationLength = self.count - maximumLength
let pivotIndex = self.index(startIndex, offsetBy: pivot)
let startIndex = self.index(pivotIndex, offsetBy: -truncationLength / 2)
let endIndex = self.index(pivotIndex, offsetBy: truncationLength / 2)
return replacingCharacters(in: startIndex..<endIndex, with: "")
}
}
extension URL {
internal var securityOrigin: String {
// TODO: This could be smarter
return self.host() ?? ""
}
}
+9
View File
@@ -7,6 +7,15 @@
import UIKit
infix operator .. : AssignmentPrecedence
@discardableResult @inline(__always) @inlinable
public func .. <T: Any>(it: T, apply: (inout T) throws -> Void) rethrows -> T {
var it = it
try apply(&it)
return it
}
protocol Conf { }
extension Conf {
@@ -36,8 +36,6 @@ NS_SWIFT_NAME(ProcessBundleBridge)
@property (nonatomic, assign) BOOL allowAllScripts; // default is NO
@property (nonatomic, assign) BOOL darkModeEnabled;
@property (nonatomic, readonly) BOOL webContentProcessConnected;
- (instancetype)initWithWebViewConfiguration:(nullable WKWebViewConfiguration *)webViewConfiguration NS_DESIGNATED_INITIALIZER;
- (void)policyDataSourceDidChange;
@@ -7,20 +7,120 @@
#import "SBRProcessBundleBridge.h"
#import "SBRWebProcessDelegate.h"
#import "SBRWebProcessProxy.h"
#import "SBRScriptPolicy.h"
#import "Hacks.h"
#import <OSLog/OSLog.h>
#import <stdatomic.h>
#import <WebKit/_WKRemoteObjectInterface.h>
#import <WebKit/_WKRemoteObjectRegistry.h>
#import <WebKit/_WKProcessPoolConfiguration.h>
#import <WebKit/_WKUserStyleSheet.h>
#import <WebKit/WKProcessPoolPrivate.h>
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/WKWebViewConfigurationPrivate.h>
#import <WebKit/WKUserContentControllerPrivate.h>
#import <WebKit/WKContentRuleListStore.h>
@interface SBRProcessBundleBridge () <SBRWebProcessDelegate>
#define WKUserStyleSheet id
#define WKUserStyleSheetEncodedClassName "X1dLVXNlclN0eWxlU2hlZXQ="
NSArray<NSString *> *CommonCDNList(void) {
static dispatch_once_t onceToken;
static NSArray<NSString *> *commonCDNList = nil;
dispatch_once(&onceToken, ^{
commonCDNList = @[
@"cdn.jsdelivr.net",
@"fsdn.net",
@"cdnjs.com",
@"osscdn.com",
@"code.jquery.com",
@"bootstrapcdn.com"
];
});
return commonCDNList;
}
@interface StyleSheet : NSObject <NSCopying>
@property (nonatomic, readonly, copy) NSString *source;
@property (nonatomic, readonly, copy) NSURL *baseURL;
@property (nonatomic, readonly, getter=isForMainFrameOnly) BOOL forMainFrameOnly;
- (instancetype)initWithSource:(NSString *)source forMainFrameOnly:(BOOL)forMainFrameOnly;
@end
// Note: We no longer proxy http/https via WKURLSchemeHandler; we use
// WebKit content blocking rules instead.
@interface WKUserContentController (Private)
- (void)__addUserStyleSheet:(WKUserStyleSheet)userStyleSheet;
- (void)__removeUserStyleSheet:(WKUserStyleSheet)userStyleSheet;
- (void)__addUserScriptImmediately:(WKUserScript *)userScript;
@end
@implementation WKUserContentController (Private)
- (void)__addUserStyleSheet:(id)userStyleSheet
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:DecodedSelector("X2FkZFVzZXJTdHlsZVNoZWV0Og==") withObject:userStyleSheet];
#pragma clang diagnostic pop
}
- (void)__removeUserStyleSheet:(id)userStyleSheet
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:DecodedSelector("X3JlbW92ZVVzZXJTdHlsZVNoZWV0Og==") withObject:userStyleSheet];
#pragma clang diagnostic pop
}
- (void)__addUserScriptImmediately:(WKUserScript *)userScript
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:DecodedSelector("X2FkZFVzZXJTY3JpcHRJbW1lZGlhdGVseTo=") withObject:userScript];
#pragma clang diagnostic pop
}
@end
#define LOG_DEBUG(format, ...) os_log_debug(_log, format, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) os_log_error(_log, format, ##__VA_ARGS__)
static os_log_t _log;
__attribute__((constructor))
static void initialize_log(void) {
_log = os_log_create("net.buzzert.attractor.webview", "bridge");
}
@interface NSURLResponse (BridgeAdditions)
@property (nonatomic, readonly) BOOL isJavascriptResponse;
@end
@implementation NSURLResponse (BridgeAdditions)
- (BOOL)isJavascriptResponse
{
NSString *extension = [[self URL] pathExtension];
if ([[extension lowercaseString] isEqualToString:@"js"]) {
return YES;
}
NSString *MIMEType = [self MIMEType];
if ([[MIMEType lowercaseString] containsString:@"javascript"]) {
return YES;
}
return NO;
}
@end
@interface SBRProcessBundleBridge ()
@end
@@ -28,43 +128,27 @@
WKWebView *_webView;
WKWebViewConfiguration *_webViewConfiguration;
WKProcessPool *_processPool;
id<SBRWebProcessProxy> _webProcessProxy;
_WKUserStyleSheet *_darkModeStyleSheet;
WKUserStyleSheet _darkModeStyleSheet;
WKUserScript *_readabilityScript;
NSArray<WKUserScript *> *_userScripts;
// These come from settings.
_WKUserStyleSheet *_customizedUserStylesheet;
WKUserStyleSheet _customizedUserStylesheet;
WKUserScript *_customizedUserScript;
// Content blocking
WKContentRuleList *_activeScriptRuleList;
void *_urlKVOContext;
}
- (void)tearDown
{
[[_webView _remoteObjectRegistry] unregisterExportedObject:self interface:[self _webProcessDelegateInterface]];
}
- (_WKRemoteObjectInterface *)_webProcessDelegateInterface
{
static dispatch_once_t onceToken;
static _WKRemoteObjectInterface *interface = nil;
dispatch_once(&onceToken, ^{
interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(SBRWebProcessDelegate)];
});
return interface;
}
- (_WKRemoteObjectInterface *)_webProcessProxyInterface
{
static dispatch_once_t onceToken;
static _WKRemoteObjectInterface *interface = nil;
dispatch_once(&onceToken, ^{
interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(SBRWebProcessProxy)];
});
return interface;
if (_webView) {
@try { [_webView removeObserver:self forKeyPath:@"URL" context:_urlKVOContext]; }
@catch (__unused NSException *ex) {}
}
}
- (instancetype)initWithWebViewConfiguration:(WKWebViewConfiguration *)webViewConfiguration
@@ -74,30 +158,13 @@
if (!webViewConfiguration) {
webViewConfiguration = [[WKWebViewConfiguration alloc] init];
// Inject bundle
_WKProcessPoolConfiguration *poolConfiguration = [[_WKProcessPoolConfiguration alloc] init];
NSURL *bundleURL = [[[NSBundle mainBundle] builtInPlugInsURL] URLByAppendingPathComponent:@"SBrowserProcessBundle.bundle"];
// Make sure it exists. Bail if otherwise.
NSBundle *pluginBundle = [NSBundle bundleWithURL:bundleURL];
NSAssert(pluginBundle != nil, @"Attix process bundle not found at %@", bundleURL.path);
[poolConfiguration setInjectedBundleURL:bundleURL];
// Set up process pool
_processPool = [[WKProcessPool alloc] _initWithConfiguration:poolConfiguration];
// Initialize allowed origins now
NSDictionary<NSString *, NSNumber *> *policies = [_policyDataSource scriptPolicyTypeByOrigin];
NSArray<NSString *> *allowedOrigins = [[_policyDataSource allowedOriginsForScriptResources] allObjects];
[_processPool _setObject:allowedOrigins forBundleParameter:SBRGetAllowedOriginsKey()];
[_processPool _setObject:@(_allowAllScripts) forBundleParameter:SBRGetAllScriptsAllowedKey()];
[_processPool _setObject:policies forBundleParameter:SBRGetPolicyTypeByOriginKey()];
_processPool = [[WKProcessPool alloc] init];
webViewConfiguration.processPool = _processPool;
webViewConfiguration._waitsForPaintAfterViewDidMoveToWindow = NO;
webViewConfiguration._applePayEnabled = YES;
// No http/https interception — rely on content blocking rules instead.
}
_webViewConfiguration = webViewConfiguration;
@@ -113,17 +180,16 @@
// Instantiate web view
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:webViewConfiguration];
if ([webView respondsToSelector:@selector(_setFindInteractionEnabled:)]) {
webView._findInteractionEnabled = YES;
if (@available(iOS 16.0, *)) {
webView.findInteractionEnabled = YES;
}
// Configure proxy interface (interface to remote web process)
_webProcessProxy = [[webView _remoteObjectRegistry] remoteObjectProxyWithInterface:[self _webProcessProxyInterface]];
// Configure delegate interface (registering us as the web process delegate for the remote process)
[[webView _remoteObjectRegistry] registerExportedObject:self interface:[self _webProcessDelegateInterface]];
_webView = webView;
_urlKVOContext = &_urlKVOContext; // unique context pointer
[_webView addObserver:self forKeyPath:@"URL" options:(NSKeyValueObservingOptionNew) context:_urlKVOContext];
// Initialize content blocking rules for current host
[self rebuildContentBlockingRulesForCurrentHost];
}
return self;
@@ -151,9 +217,7 @@
- (void)reloadCustomizedUserScriptsAndStylesheets
{
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
if (_customizedUserScript) {
[userContentController _removeUserScript:_customizedUserScript];
}
[userContentController removeAllUserScripts];
NSString *scriptSource = [[NSUserDefaults standardUserDefaults] stringForKey:@"userScript"];
if ([scriptSource length]) {
@@ -162,55 +226,44 @@
}
if (_customizedUserStylesheet) {
[userContentController _removeUserStyleSheet:_customizedUserStylesheet];
[userContentController __removeUserStyleSheet:_customizedUserStylesheet];
}
NSString *stylesheetSource = [[NSUserDefaults standardUserDefaults] stringForKey:@"userStylesheet"];
if ([stylesheetSource length]) {
_customizedUserStylesheet = [[_WKUserStyleSheet alloc] initWithSource:stylesheetSource forMainFrameOnly:YES];
[userContentController _addUserStyleSheet:_customizedUserStylesheet];
_customizedUserStylesheet = [[DecodedClass(WKUserStyleSheetEncodedClassName) alloc] initWithSource:stylesheetSource forMainFrameOnly:YES];
[userContentController __addUserStyleSheet:_customizedUserStylesheet];
}
}
#pragma mark <SBRWebProcessDelegate>
- (void)webProcessDidConnect
{
NSLog(@"SBRProcessBundleBridge: did connect. Saying hello, syncing allowlist");
_webContentProcessConnected = YES;
[_webProcessProxy hello];
[self policyDataSourceDidChange];
}
#pragma mark Former <SBRWebProcessDelegate> methods
- (void)webProcessDidAllowScriptWithOrigin:(NSString *)origin
{
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webProcess:self didAllowScriptResourceFromOrigin:origin];
});
}
- (void)webProcessDidBlockScriptWithOrigin:(NSString *)origin
{
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webProcess:self didBlockScriptResourceFromOrigin:origin];
});
}
#pragma mark Actions
- (void)policyDataSourceDidChange
{
NSArray<NSString *> *allowedOrigins = [[_policyDataSource allowedOriginsForScriptResources] allObjects];
[_processPool _setObject:allowedOrigins forBundleParameter:SBRGetAllowedOriginsKey()];
[_webProcessProxy syncAllowedResourceOrigins:allowedOrigins];
NSDictionary<NSString *, NSNumber *> *policyTypes = [_policyDataSource scriptPolicyTypeByOrigin];
[_processPool _setObject:policyTypes forBundleParameter:SBRGetPolicyTypeByOriginKey()];
[_webProcessProxy syncPolicyTypes:policyTypes];
// Rebuild content blocking rules when policy changes.
[self rebuildContentBlockingRulesForCurrentHost];
}
- (void)setAllowAllScripts:(BOOL)allowAllScripts
{
_allowAllScripts = allowAllScripts;
[_processPool _setObject:@(_allowAllScripts) forBundleParameter:SBRGetAllScriptsAllowedKey()];
[_webProcessProxy setAllScriptsAllowed:allowAllScripts];
[self rebuildContentBlockingRulesForCurrentHost];
}
- (void)setDarkModeEnabled:(BOOL)darkModeEnabled
@@ -223,16 +276,15 @@
if (!_darkModeStyleSheet) {
NSURL *styleSheetURL = [[NSBundle mainBundle] URLForResource:@"darkmode" withExtension:@"css"];
NSString *styleSheetSource = [NSString stringWithContentsOfURL:styleSheetURL encoding:NSUTF8StringEncoding error:nil];
_darkModeStyleSheet = [[_WKUserStyleSheet alloc] initWithSource:styleSheetSource forMainFrameOnly:NO];
_darkModeStyleSheet = [[DecodedClass(WKUserStyleSheetEncodedClassName) alloc] initWithSource:styleSheetSource forMainFrameOnly:NO];
}
[userContentController _addUserStyleSheet:_darkModeStyleSheet];
[userContentController __addUserStyleSheet:_darkModeStyleSheet];
} else if (_darkModeStyleSheet) {
[userContentController _removeUserStyleSheet:_darkModeStyleSheet];
[userContentController __removeUserStyleSheet:_darkModeStyleSheet];
}
}
- (void)parseDocumentForReaderMode:(void (^)(NSString * _Nonnull))completionBlock
{
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
@@ -241,20 +293,136 @@
_readabilityScript = [self _loadScriptForResource:@"Readability" withExtension:@"js"];
}
[userContentController _addUserScriptImmediately:_readabilityScript];
[userContentController __addUserScriptImmediately:_readabilityScript];
NSString *script = @""
"var documentClone = document.cloneNode(true);"
"var article = new Readability(documentClone).parse();"
"article.content";
os_log_t log = _log;
[_webView evaluateJavaScript:script completionHandler:^(NSString *result, NSError * _Nullable error) {
if (error != nil) {
NSLog(@"Bridge: Readability error: %@", error.localizedDescription);
os_log_error(log, "Bridge: Readability error: %@", error.localizedDescription);
} else {
completionBlock(result);
}
}];
}
// MARK: - Content Blocking Rules
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if (context == _urlKVOContext && [keyPath isEqualToString:@"URL"]) {
[self rebuildContentBlockingRulesForCurrentHost];
return;
}
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
- (void)rebuildContentBlockingRulesForCurrentHost
{
NSString *hostOrigin = _webView.URL.host;
if (!hostOrigin) {
// No page loaded yet; clear any existing rules
[self applyContentRuleListJSON:nil withName:nil];
return;
}
// Determine policy for this host
NSNumber *policyType = nil;
@synchronized (_policyDataSource) {
NSDictionary<NSString *, NSNumber *> *policyTypes = [_policyDataSource scriptPolicyTypeByOrigin];
policyType = [policyTypes objectForKey:hostOrigin] ?: @(0);
}
// Alpha=0 Bravo=1 Charlie=2 Delta=3 Echo=4
SBRScriptOriginPolicyType type = [policyType integerValue];
if (_allowAllScripts || type >= SBRScriptOriginPolicyTypeEcho) {
// Echo or shields down: no blocking
[self applyContentRuleListJSON:nil withName:nil];
return;
}
// Base trigger applies only to this page's domain
NSMutableArray *rules = [NSMutableArray array];
NSDictionary *baseScriptTrigger = @{
@"resource-type" : @[ @"script" ],
@"if-domain" : @[ hostOrigin ]
};
if (type <= SBRScriptOriginPolicyTypeBravo) {
// Alpha or Bravo: block all external script subresources (inline JS is controlled via preferences elsewhere)
[rules addObject:@{ @"trigger": baseScriptTrigger, @"action": @{ @"type": @"block" } }];
} else {
// Charlie/Delta: block third-party scripts
NSMutableDictionary *trigger = [baseScriptTrigger mutableCopy];
trigger[@"load-type"] = @[ @"third-party" ];
[rules addObject:@{ @"trigger": trigger, @"action": @{ @"type": @"block" } }];
if (type >= SBRScriptOriginPolicyTypeDelta) {
// Delta: add allowlist for common CDNs
for (NSString *cdn in CommonCDNList()) {
NSString *escaped = [NSRegularExpression escapedPatternForString:cdn];
NSString *pattern = [NSString stringWithFormat:@".*://([^.]*\\.)?%@/", escaped];
[rules addObject:@{
@"trigger": @{
@"url-filter": pattern,
@"if-domain": @[ hostOrigin ],
@"resource-type": @[ @"script" ]
},
@"action": @{ @"type": @"ignore-previous-rules" }
}];
}
// Heuristic: allow cdn.<anything-with-family-name>/* where family name is the second-level label
NSArray<NSString *> *components = [hostOrigin componentsSeparatedByString:@"."];
if (components.count > 1) {
NSString *family = components[components.count - 2];
NSString *escapedFamily = [NSRegularExpression escapedPatternForString:family];
NSString *familyPattern = [NSString stringWithFormat:@".*://cdn\\.[^/]*%@[^/]*/", escapedFamily];
[rules addObject:@{
@"trigger": @{
@"url-filter": familyPattern,
@"if-domain": @[ hostOrigin ],
@"resource-type": @[ @"script" ]
},
@"action": @{ @"type": @"ignore-previous-rules" }
}];
}
}
}
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:rules options:0 error:nil];
NSString *json = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSString *name = [NSString stringWithFormat:@"net.buzzert.attractor.rules.%@", hostOrigin];
[self applyContentRuleListJSON:json withName:name];
}
- (void)applyContentRuleListJSON:(NSString *)json withName:(NSString *)name
{
WKUserContentController *controller = [_webViewConfiguration userContentController];
if (_activeScriptRuleList) {
[controller removeContentRuleList:_activeScriptRuleList];
_activeScriptRuleList = nil;
}
if (!json || !name) { return; }
WKContentRuleListStore *store = [WKContentRuleListStore defaultStore];
[store compileContentRuleListForIdentifier:name encodedContentRuleList:json completionHandler:^(WKContentRuleList * _Nullable ruleList, NSError * _Nullable error) {
if (error) {
LOG_ERROR("Failed to compile content rule list: %@", error.localizedDescription);
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
self->_activeScriptRuleList = ruleList;
[controller addContentRuleList:ruleList];
LOG_DEBUG("Applied content rule list: %@", name);
});
}];
}
@end
@@ -77,6 +77,10 @@ static CGFloat RoundToScale(CGFloat v, CGFloat s) {
+ (UIImage *)iconRepresentationForPolicyType:(SBRScriptOriginPolicyType)policyType withConfiguration:(nonnull SBRScriptPolicyIconConfiguration *)configuration
{
const CGSize size = [configuration size];
if (size.width < CGFLOAT_EPSILON || size.height < CGFLOAT_EPSILON) {
return nil;
}
UIFont *font = [UIFont boldSystemFontOfSize:size.height - 2];
NSDictionary<NSAttributedStringKey, id> *attrs = @{
NSFontAttributeName : font,
+12
View File
@@ -9,6 +9,18 @@ import Foundation
class SearchProvider
{
// Build a provider from a URL template. Template should contain %q or %s
// which will be replaced with the sanitized query string.
static func fromTemplate(_ template: String) -> SearchProvider {
SearchProvider(resolver: { query in
let sanitized = query.sanitized()
let replaced = template
.replacingOccurrences(of: "%q", with: sanitized)
.replacingOccurrences(of: "%s", with: sanitized)
return URL(string: replaced) ?? URL(string: "https://google.com/search?q=\(sanitized)&gbv=1")!
})
}
static let google = SearchProvider(resolver: { query in
// gbv=1: no JS
URL(string: "https://google.com/search?q=\(query.sanitized())&gbv=1")!
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"
+231
View File
@@ -0,0 +1,231 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.9)
abbrev (0.1.2)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.2)
aws-partitions (1.1109.0)
aws-sdk-core (3.224.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.101.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.188.0)
aws-sdk-core (~> 3, >= 3.224.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
csv (3.3.5)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.109.0)
faraday (1.10.6)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.2.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.4)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.1)
fastlane (2.230.0)
CFPropertyList (>= 2.3, < 4.0.0)
abbrev (~> 0.1.2)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
base64 (~> 0.2.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
logger (>= 1.6, < 2.0)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3.0)
naturally (~> 2.2)
nkf (~> 0.2.0)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.29.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.1)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.45.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.29.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.7.6)
jwt (2.10.3)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.8.1)
os (1.1.4)
plist (3.7.2)
public_suffix (5.1.1)
rake (13.4.2)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.8.0)
rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.18.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.2.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
fastlane
BUNDLED WITH
2.5.23
+35
View File
@@ -0,0 +1,35 @@
# Attractor
Attractor is a web browser for iOS and macOS prioritizing security and aggresive content filtering, and some nifty useful features.
![](Screenshots/main.png)
## Security Modes
Attractor aggresively filters unwanted web content and prevents arbitrary code execution (JavaScript) by default. I created a special browser for this purpose. Philosophically speaking, I believe the web should primarily be used as a document viewer (as originally intended by [Tim Berners-Lee](https://en.wikipedia.org/wiki/World_wide_web)). Interactive web applications are fine, but the *default* mode should be document viewing, which is why this security model exists.
Attractor operates in five "security modes":
1. **Alpha** (Default) All JavaScript is blocked, as well as web fonts and iframes.
2. **Bravo** Only embedded scripts are allowed. This means any JavaScript enclosed in `<script>` tags. Often this is enough to allow form validation to occur without issue.
3. **Charlie** Allows external JavaScript, but only from the same security origin. Meaning, if browsing `buzzert.net`, only external scripts from the domain `buzzert.net` (or subdomains) are allowed to execute.
4. **Delta** Allows external JavaScript, but only from a curated list of common CDNs.
5. **Echo** All scripts are allowed.
## Other features
- Dark mode (a very good one that uses low-level stylesheet injection to prevent flicker)
- Tab syncing
- Share via Email
- Reader Mode
- Arbitrary zoom scale
- User-defined scripts and stylesheets via UI
- Custom redirect rules
- Catalyst and iPadOS support
## Deployment
TestFlight deployment is handled by Fastlane and the Gitea workflow at `.gitea/workflows/testflight.yml`.
Required Gitea secrets match the local `.env.example`: `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_ISSUER_ID`, `APP_STORE_CONNECT_KEY_CONTENT`, `MATCH_GIT_URL`, `MATCH_PASSWORD`, and `MATCH_GIT_BASIC_AUTHORIZATION`.
Push a tag like `release/ios/v4.2` or `Attractor-4.2` to build and upload to TestFlight. To prepare signing assets locally, run `bundle exec fastlane ios setup_signing`; to upload manually, run `bundle exec fastlane ios beta`.
+74 -142
View File
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -27,10 +27,7 @@
1ADFF46C24C7DE54006DC7AE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1ADFF46A24C7DE54006DC7AE /* LaunchScreen.storyboard */; };
1ADFF47424C7DE9C006DC7AE /* BrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF47324C7DE9C006DC7AE /* BrowserViewController.swift */; };
1ADFF47924C7DFF8006DC7AE /* BrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF47824C7DFF8006DC7AE /* BrowserView.swift */; };
1ADFF48424C8C12F006DC7AE /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ADFF48324C8C12F006DC7AE /* Foundation.framework */; };
1ADFF48D24C8C176006DC7AE /* SBRProcessBundleBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF48C24C8C176006DC7AE /* SBRProcessBundleBridge.m */; };
1ADFF4A724C8C271006DC7AE /* SBrowserProcessBundle.bundle in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 1ADFF48124C8C12F006DC7AE /* SBrowserProcessBundle.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1ADFF4AA24C8D477006DC7AE /* SBRProcessPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4A924C8D477006DC7AE /* SBRProcessPlugin.m */; };
1ADFF4AE24C8ED32006DC7AE /* ResourcePolicyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4AD24C8ED32006DC7AE /* ResourcePolicyManager.swift */; };
1ADFF4C024CA6964006DC7AE /* URLBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4BF24CA6964006DC7AE /* URLBar.swift */; };
1ADFF4C324CA6AF6006DC7AE /* Geometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4C224CA6AF6006DC7AE /* Geometry.swift */; };
@@ -40,11 +37,15 @@
1ADFF4CD24CBB0C8006DC7AE /* ScriptOriginPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CC24CBB0C8006DC7AE /* ScriptOriginPolicyViewController.swift */; };
CD01D5A5254A10BB00189CDC /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD01D5A4254A10BB00189CDC /* TabBarView.swift */; };
CD01D5AB254A206D00189CDC /* TabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD01D5AA254A206D00189CDC /* TabBarViewController.swift */; };
CD15332F297B6798009A7F3A /* HistoryBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD15332E297B6798009A7F3A /* HistoryBrowserViewController.swift */; };
CD153331297B6806009A7F3A /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD153330297B6806009A7F3A /* HistoryView.swift */; };
CD16844D269E709400B8F8A5 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD16844C269E709400B8F8A5 /* Box.swift */; };
CD19576D268BE95900E8089B /* GenericContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD19576C268BE95900E8089B /* GenericContentView.swift */; };
CD361CF6271A3718006E9CA5 /* SBRScriptPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = CD361CF5271A3718006E9CA5 /* SBRScriptPolicy.m */; };
CD470C4225DE056600AFBE0E /* BrowserViewController+WebKitDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD470C4125DE056600AFBE0E /* BrowserViewController+WebKitDelegate.swift */; };
CD470C4425DE070400AFBE0E /* BrowserViewController+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD470C4325DE070400AFBE0E /* BrowserViewController+Keyboard.swift */; };
CD4930D92E8B390200ADDE99 /* AddSearchEngineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4930D82E8B38FC00ADDE99 /* AddSearchEngineViewController.swift */; };
CD4930DB2E8B3F6500ADDE99 /* ManageSearchEnginesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4930DA2E8B3F5B00ADDE99 /* ManageSearchEnginesViewController.swift */; };
CD7313E22705349700053347 /* ScriptPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7313E12705349700053347 /* ScriptPolicyViewController.swift */; };
CD7313E4270534B800053347 /* ScriptPolicyViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7313E3270534B800053347 /* ScriptPolicyViewControllerDelegate.swift */; };
CD7A7E9D2686A9A500E20BA3 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7A7E9C2686A9A500E20BA3 /* SettingsViewController.swift */; };
@@ -57,10 +58,12 @@
CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCD24E7763900D2BDCC /* BrowserHistory.swift */; };
CD853BD124E778B800D2BDCC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */; };
CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BD324E77BF900D2BDCC /* HistoryItem.swift */; };
CD8ACBC22DC9A2F7008BF856 /* Hacks.m in Sources */ = {isa = PBXBuildFile; fileRef = CD8ACBC12DC9A2F7008BF856 /* Hacks.m */; };
CD8DBE7B2A85D892006A0FE0 /* LayoutLatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8DBE7A2A85D892006A0FE0 /* LayoutLatch.swift */; };
CD936A3B289DB3380093A1AC /* TabInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD936A3A289DB3380093A1AC /* TabInfo.swift */; };
CD936A3D289DB88B0093A1AC /* UIView+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD936A3C289DB88B0093A1AC /* UIView+Utils.swift */; };
CD97CF9225D5BE6F00288FEE /* NavigationControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD97CF9125D5BE6F00288FEE /* NavigationControlsView.swift */; };
CD9B88C2272201E900DAAB7E /* SBRScriptPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = CD361CF5271A3718006E9CA5 /* SBRScriptPolicy.m */; };
CDACE18C2C5B09F00053AB29 /* StringUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDACE18B2C5B09F00053AB29 /* StringUtilities.swift */; };
CDAD9CE8263A2DF200FF7199 /* DocumentControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAD9CE7263A2DF200FF7199 /* DocumentControlsView.swift */; };
CDAD9CEA263A318F00FF7199 /* ShareableURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAD9CE9263A318F00FF7199 /* ShareableURL.swift */; };
CDB6807D28B446FC007D787E /* ReaderSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB6807C28B446FC007D787E /* ReaderSceneDelegate.swift */; };
@@ -83,16 +86,6 @@
CDF34690276C14BD00FB3141 /* CodeEditorSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
1ADFF48E24C8C230006DC7AE /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 1ADFF45424C7DE53006DC7AE /* Project object */;
proxyType = 1;
remoteGlobalIDString = 1ADFF48024C8C12F006DC7AE;
remoteInfo = SBrowserProcessBundle;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
1ADFF4A124C8C25B006DC7AE /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
@@ -100,7 +93,6 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
1ADFF4A724C8C271006DC7AE /* SBrowserProcessBundle.bundle in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
@@ -131,16 +123,10 @@
1ADFF47324C7DE9C006DC7AE /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = "<group>"; };
1ADFF47524C7DF20006DC7AE /* SBrowser.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SBrowser.entitlements; sourceTree = "<group>"; };
1ADFF47824C7DFF8006DC7AE /* BrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserView.swift; sourceTree = "<group>"; };
1ADFF48124C8C12F006DC7AE /* SBrowserProcessBundle.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SBrowserProcessBundle.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
1ADFF48324C8C12F006DC7AE /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
1ADFF48624C8C12F006DC7AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1ADFF48A24C8C176006DC7AE /* SBrowser-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SBrowser-Bridging-Header.h"; sourceTree = "<group>"; };
1ADFF48B24C8C176006DC7AE /* SBRProcessBundleBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SBRProcessBundleBridge.h; sourceTree = "<group>"; };
1ADFF48C24C8C176006DC7AE /* SBRProcessBundleBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SBRProcessBundleBridge.m; sourceTree = "<group>"; };
1ADFF4A824C8D476006DC7AE /* SBRProcessPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SBRProcessPlugin.h; sourceTree = "<group>"; };
1ADFF4A924C8D477006DC7AE /* SBRProcessPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SBRProcessPlugin.m; sourceTree = "<group>"; };
1ADFF4AB24C8DF62006DC7AE /* SBRWebProcessDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SBRWebProcessDelegate.h; sourceTree = "<group>"; };
1ADFF4AC24C8DFEE006DC7AE /* SBRWebProcessProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SBRWebProcessProxy.h; sourceTree = "<group>"; };
1ADFF4AD24C8ED32006DC7AE /* ResourcePolicyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcePolicyManager.swift; sourceTree = "<group>"; };
1ADFF4BF24CA6964006DC7AE /* URLBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBar.swift; sourceTree = "<group>"; };
1ADFF4C224CA6AF6006DC7AE /* Geometry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Geometry.swift; sourceTree = "<group>"; };
@@ -150,12 +136,17 @@
1ADFF4CC24CBB0C8006DC7AE /* ScriptOriginPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptOriginPolicyViewController.swift; sourceTree = "<group>"; };
CD01D5A4254A10BB00189CDC /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = "<group>"; };
CD01D5AA254A206D00189CDC /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = "<group>"; };
CD15332E297B6798009A7F3A /* HistoryBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryBrowserViewController.swift; sourceTree = "<group>"; };
CD153330297B6806009A7F3A /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
CD16844C269E709400B8F8A5 /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; };
CD19576C268BE95900E8089B /* GenericContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericContentView.swift; sourceTree = "<group>"; };
CD361CF4271A3718006E9CA5 /* SBRScriptPolicy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SBRScriptPolicy.h; sourceTree = "<group>"; };
CD361CF5271A3718006E9CA5 /* SBRScriptPolicy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SBRScriptPolicy.m; sourceTree = "<group>"; };
CD3D6CED2DA9F8910099667F /* WebKitDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WebKitDefines.h; sourceTree = "<group>"; };
CD470C4125DE056600AFBE0E /* BrowserViewController+WebKitDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+WebKitDelegate.swift"; sourceTree = "<group>"; };
CD470C4325DE070400AFBE0E /* BrowserViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+Keyboard.swift"; sourceTree = "<group>"; };
CD4930D82E8B38FC00ADDE99 /* AddSearchEngineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSearchEngineViewController.swift; sourceTree = "<group>"; };
CD4930DA2E8B3F5B00ADDE99 /* ManageSearchEnginesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageSearchEnginesViewController.swift; sourceTree = "<group>"; };
CD7313E12705349700053347 /* ScriptPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewController.swift; sourceTree = "<group>"; };
CD7313E3270534B800053347 /* ScriptPolicyViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewControllerDelegate.swift; sourceTree = "<group>"; };
CD7A7E9C2686A9A500E20BA3 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
@@ -168,9 +159,13 @@
CD853BCD24E7763900D2BDCC /* BrowserHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserHistory.swift; sourceTree = "<group>"; };
CD853BD024E778B800D2BDCC /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = "<group>"; };
CD853BD324E77BF900D2BDCC /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = "<group>"; };
CD8ACBC02DC9A2F7008BF856 /* Hacks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Hacks.h; sourceTree = "<group>"; };
CD8ACBC12DC9A2F7008BF856 /* Hacks.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Hacks.m; sourceTree = "<group>"; };
CD8DBE7A2A85D892006A0FE0 /* LayoutLatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutLatch.swift; sourceTree = "<group>"; };
CD936A3A289DB3380093A1AC /* TabInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabInfo.swift; sourceTree = "<group>"; };
CD936A3C289DB88B0093A1AC /* UIView+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Utils.swift"; sourceTree = "<group>"; };
CD97CF9125D5BE6F00288FEE /* NavigationControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControlsView.swift; sourceTree = "<group>"; };
CDACE18B2C5B09F00053AB29 /* StringUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtilities.swift; sourceTree = "<group>"; };
CDAD9CE7263A2DF200FF7199 /* DocumentControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentControlsView.swift; sourceTree = "<group>"; };
CDAD9CE9263A318F00FF7199 /* ShareableURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareableURL.swift; sourceTree = "<group>"; };
CDB6807C28B446FC007D787E /* ReaderSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderSceneDelegate.swift; sourceTree = "<group>"; };
@@ -193,6 +188,10 @@
CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditorSettingsViewController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
CDB73F3F2DA9F2F100DF4FAB /* WebKit Headers */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "WebKit Headers"; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
1ADFF45924C7DE53006DC7AE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -201,14 +200,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
1ADFF47E24C8C12F006DC7AE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1ADFF48424C8C12F006DC7AE /* Foundation.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -275,7 +266,7 @@
children = (
CDB6808828B4569F007D787E /* LICENSE.txt */,
1ADFF45E24C7DE53006DC7AE /* App */,
1ADFF48524C8C12F006DC7AE /* SBrowserProcessBundle */,
CDB73F3F2DA9F2F100DF4FAB /* WebKit Headers */,
1ADFF48224C8C12F006DC7AE /* Frameworks */,
1ADFF45D24C7DE53006DC7AE /* Products */,
);
@@ -285,7 +276,6 @@
isa = PBXGroup;
children = (
1ADFF45C24C7DE53006DC7AE /* Attractor.app */,
1ADFF48124C8C12F006DC7AE /* SBrowserProcessBundle.bundle */,
);
name = Products;
sourceTree = "<group>";
@@ -302,6 +292,7 @@
CDCE2662251AA7FC007FE92A /* Document Controls UI */,
1AD3104125254FA300A4A952 /* Find on Page */,
CD7F2133265DACEE0001D042 /* Hacks */,
CD15332D297B6784009A7F3A /* History UI */,
CDC5DA3C25DB7A5500BA8D99 /* Reader View */,
CDB6807B28B4456B007D787E /* Scene Delegates */,
1ADFF4CE24CBBCBD006DC7AE /* Script Policy UI */,
@@ -326,6 +317,7 @@
1ADFF46824C7DE54006DC7AE /* Assets.xcassets */,
1ADFF46A24C7DE54006DC7AE /* LaunchScreen.storyboard */,
1ADFF46D24C7DE54006DC7AE /* Info.plist */,
CD3D6CED2DA9F8910099667F /* WebKitDefines.h */,
);
path = "Supporting Files";
sourceTree = "<group>";
@@ -358,18 +350,6 @@
name = Frameworks;
sourceTree = "<group>";
};
1ADFF48524C8C12F006DC7AE /* SBrowserProcessBundle */ = {
isa = PBXGroup;
children = (
1ADFF48624C8C12F006DC7AE /* Info.plist */,
1ADFF4AB24C8DF62006DC7AE /* SBRWebProcessDelegate.h */,
1ADFF4AC24C8DFEE006DC7AE /* SBRWebProcessProxy.h */,
1ADFF4A824C8D476006DC7AE /* SBRProcessPlugin.h */,
1ADFF4A924C8D477006DC7AE /* SBRProcessPlugin.m */,
);
path = SBrowserProcessBundle;
sourceTree = "<group>";
};
1ADFF4AF24C92E2F006DC7AE /* Web Process Bundle Bridge */ = {
isa = PBXGroup;
children = (
@@ -391,6 +371,8 @@
1AB88F0524D4D3A90006F850 /* UIGestureRecognizer+Actions.swift */,
CDEDD8A925D62ADB00862605 /* UITraitCollection+MacLike.swift */,
CD936A3C289DB88B0093A1AC /* UIView+Utils.swift */,
CD8DBE7A2A85D892006A0FE0 /* LayoutLatch.swift */,
CDACE18B2C5B09F00053AB29 /* StringUtilities.swift */,
);
path = Utilities;
sourceTree = "<group>";
@@ -406,6 +388,15 @@
path = "Script Policy UI";
sourceTree = "<group>";
};
CD15332D297B6784009A7F3A /* History UI */ = {
isa = PBXGroup;
children = (
CD15332E297B6798009A7F3A /* HistoryBrowserViewController.swift */,
CD153330297B6806009A7F3A /* HistoryView.swift */,
);
path = "History UI";
sourceTree = "<group>";
};
CD7A7E9B2686A99600E20BA3 /* Amber */ = {
isa = PBXGroup;
children = (
@@ -426,6 +417,8 @@
CD7F2133265DACEE0001D042 /* Hacks */ = {
isa = PBXGroup;
children = (
CD8ACBC02DC9A2F7008BF856 /* Hacks.h */,
CD8ACBC12DC9A2F7008BF856 /* Hacks.m */,
CD7F2134265DAD010001D042 /* MFMailComposeViewControllerFix.m */,
);
path = Hacks;
@@ -488,6 +481,8 @@
CD7A7E9C2686A9A500E20BA3 /* SettingsViewController.swift */,
CD7A7E9E2686B29100E20BA3 /* GeneralSettingsViewController.swift */,
CD7A7EA02686B2E600E20BA3 /* RedirectRulesSettingsViewController.swift */,
CD4930D82E8B38FC00ADDE99 /* AddSearchEngineViewController.swift */,
CD4930DA2E8B3F5B00ADDE99 /* ManageSearchEnginesViewController.swift */,
CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */,
);
path = Settings;
@@ -516,30 +511,12 @@
buildRules = (
);
dependencies = (
1ADFF48F24C8C230006DC7AE /* PBXTargetDependency */,
);
name = App;
productName = SBrowser;
productReference = 1ADFF45C24C7DE53006DC7AE /* Attractor.app */;
productType = "com.apple.product-type.application";
};
1ADFF48024C8C12F006DC7AE /* SBrowserProcessBundle */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1ADFF48724C8C12F006DC7AE /* Build configuration list for PBXNativeTarget "SBrowserProcessBundle" */;
buildPhases = (
1ADFF47D24C8C12F006DC7AE /* Sources */,
1ADFF47E24C8C12F006DC7AE /* Frameworks */,
1ADFF47F24C8C12F006DC7AE /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SBrowserProcessBundle;
productName = SBrowserProcessBundle;
productReference = 1ADFF48124C8C12F006DC7AE /* SBrowserProcessBundle.bundle */;
productType = "com.apple.product-type.bundle";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -553,9 +530,6 @@
CreatedOnToolsVersion = 12.0;
LastSwiftMigration = 1200;
};
1ADFF48024C8C12F006DC7AE = {
CreatedOnToolsVersion = 12.0;
};
};
};
buildConfigurationList = 1ADFF45724C7DE53006DC7AE /* Build configuration list for PBXProject "SBrowser" */;
@@ -572,7 +546,6 @@
projectRoot = "";
targets = (
1ADFF45B24C7DE53006DC7AE /* App */,
1ADFF48024C8C12F006DC7AE /* SBrowserProcessBundle */,
);
};
/* End PBXProject section */
@@ -593,13 +566,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
1ADFF47F24C8C12F006DC7AE /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -609,8 +575,10 @@
files = (
1ADFF46024C7DE53006DC7AE /* AppDelegate.swift in Sources */,
1AD3104325254FB900A4A952 /* FindOnPageViewController.swift in Sources */,
CD4930D92E8B390200ADDE99 /* AddSearchEngineViewController.swift in Sources */,
1A03811424E73EB300826501 /* SegmentedReliefButton.swift in Sources */,
1A03811024E71CF000826501 /* ReliefButton.swift in Sources */,
CD8ACBC22DC9A2F7008BF856 /* Hacks.m in Sources */,
1A03811224E71EAA00826501 /* GradientView.swift in Sources */,
1ADFF4C024CA6964006DC7AE /* URLBar.swift in Sources */,
CDAD9CEA263A318F00FF7199 /* ShareableURL.swift in Sources */,
@@ -634,6 +602,7 @@
CDB6807D28B446FC007D787E /* ReaderSceneDelegate.swift in Sources */,
CDD0522425F8055700DD1771 /* SearchProvider.swift in Sources */,
CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */,
CD15332F297B6798009A7F3A /* HistoryBrowserViewController.swift in Sources */,
1ADFF48D24C8C176006DC7AE /* SBRProcessBundleBridge.m in Sources */,
1AB88F0624D4D3A90006F850 /* UIGestureRecognizer+Actions.swift in Sources */,
CD7313E4270534B800053347 /* ScriptPolicyViewControllerDelegate.swift in Sources */,
@@ -641,6 +610,8 @@
CD16844D269E709400B8F8A5 /* Box.swift in Sources */,
CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */,
1A03810B24E71C5600826501 /* ToolbarButtonContainerView.swift in Sources */,
CD8DBE7B2A85D892006A0FE0 /* LayoutLatch.swift in Sources */,
CD4930DB2E8B3F6500ADDE99 /* ManageSearchEnginesViewController.swift in Sources */,
CD7A7EA12686B2E600E20BA3 /* RedirectRulesSettingsViewController.swift in Sources */,
1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */,
CD7A8915251975B70075991E /* AutocompleteViewController.swift in Sources */,
@@ -654,6 +625,7 @@
CDAD9CE8263A2DF200FF7199 /* DocumentControlsView.swift in Sources */,
1ADFF4C924CA793E006DC7AE /* ToolbarViewController.swift in Sources */,
CD7A89172519872D0075991E /* KeyboardShortcuts.swift in Sources */,
CDACE18C2C5B09F00053AB29 /* StringUtilities.swift in Sources */,
1ADFF4CD24CBB0C8006DC7AE /* ScriptOriginPolicyViewController.swift in Sources */,
CDE6A30425F023BC00E912A4 /* AmberSettingsViewController.swift in Sources */,
1A14FC2824D26749009B3F83 /* Tab.swift in Sources */,
@@ -663,6 +635,7 @@
CD01D5AB254A206D00189CDC /* TabBarViewController.swift in Sources */,
1ADFF47924C7DFF8006DC7AE /* BrowserView.swift in Sources */,
CDCE2664251AA80F007FE92A /* DocumentControlViewController.swift in Sources */,
CD153331297B6806009A7F3A /* HistoryView.swift in Sources */,
CDF3468E276C105900FB3141 /* SettingsSceneDelegate.swift in Sources */,
1AB88EFF24D3BBA50006F850 /* TabPickerViewController.swift in Sources */,
CD19576D268BE95900E8089B /* GenericContentView.swift in Sources */,
@@ -672,25 +645,8 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
1ADFF47D24C8C12F006DC7AE /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CD9B88C2272201E900DAAB7E /* SBRScriptPolicy.m in Sources */,
1ADFF4AA24C8D477006DC7AE /* SBRProcessPlugin.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
1ADFF48F24C8C230006DC7AE /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 1ADFF48024C8C12F006DC7AE /* SBrowserProcessBundle */;
targetProxy = 1ADFF48E24C8C230006DC7AE /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
1ADFF46A24C7DE54006DC7AE /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
@@ -756,11 +712,12 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
HEADER_SEARCH_PATHS = "\"WebKit Headers\"";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos.internal;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
@@ -813,10 +770,11 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
HEADER_SEARCH_PATHS = "\"WebKit Headers\"";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos.internal;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
@@ -830,19 +788,27 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "App/Supporting Files/SBrowser.entitlements";
CODE_SIGN_IDENTITY = "-";
CURRENT_PROJECT_VERSION = 4;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3SJALV9BQ7;
INFOPLIST_FILE = "App/Supporting Files/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Attractor;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.0;
MARKETING_VERSION = 4.1;
PRODUCT_BUNDLE_IDENTIFIER = net.buzzert.attractor;
PRODUCT_NAME = Attractor;
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "App/Supporting Files/SBrowser-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -857,58 +823,33 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "App/Supporting Files/SBrowser.entitlements";
CODE_SIGN_IDENTITY = "-";
CURRENT_PROJECT_VERSION = 4;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3SJALV9BQ7;
INFOPLIST_FILE = "App/Supporting Files/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Attractor;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.0;
MARKETING_VERSION = 4.1;
PRODUCT_BUNDLE_IDENTIFIER = net.buzzert.attractor;
PRODUCT_NAME = Attractor;
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "App/Supporting Files/SBrowser-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6";
};
name = Release;
};
1ADFF48824C8C12F006DC7AE /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
DEAD_CODE_STRIPPING = YES;
INFOPLIST_FILE = SBrowserProcessBundle/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
PRODUCT_BUNDLE_IDENTIFIER = net.buzzert.SBrowserProcessBundle;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos.internal;
STRIP_INSTALLED_PRODUCT = NO;
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
TARGETED_DEVICE_FAMILY = "1,2,6";
WRAPPER_EXTENSION = bundle;
};
name = Debug;
};
1ADFF48924C8C12F006DC7AE /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
DEAD_CODE_STRIPPING = YES;
INFOPLIST_FILE = SBrowserProcessBundle/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
PRODUCT_BUNDLE_IDENTIFIER = net.buzzert.SBrowserProcessBundle;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos.internal;
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
TARGETED_DEVICE_FAMILY = "1,2,6";
WRAPPER_EXTENSION = bundle;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -930,15 +871,6 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
1ADFF48724C8C12F006DC7AE /* Build configuration list for PBXNativeTarget "SBrowserProcessBundle" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1ADFF48824C8C12F006DC7AE /* Debug */,
1ADFF48924C8C12F006DC7AE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCVersionGroup section */
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@@ -26,9 +27,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -39,8 +39,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
internalIOSLaunchStyle = "2">
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
@@ -51,13 +50,6 @@
ReferencedContainer = "container:SBrowser.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
<AdditionalOption
key = "MallocScribble"
value = ""
isEnabled = "YES">
</AdditionalOption>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -83,7 +75,4 @@
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
<InstallAction
buildConfiguration = "Release">
</InstallAction>
</Scheme>
-24
View File
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSPrincipalClass</key>
<string>SBRProcessPlugin</string>
</dict>
</plist>
-17
View File
@@ -1,17 +0,0 @@
//
// SBRProcessPlugin.h
// SBrowserProcessBundle
//
// Created by James Magahern on 7/22/20.
//
#import <Foundation/Foundation.h>
#import <WebKit/WKWebProcessPlugIn.h>
NS_ASSUME_NONNULL_BEGIN
@interface SBRProcessPlugin : NSObject <WKWebProcessPlugIn>
@end
NS_ASSUME_NONNULL_END
-115
View File
@@ -1,115 +0,0 @@
//
// SBRProcessPlugin.m
// SBrowserProcessBundle
//
// Created by James Magahern on 7/22/20.
//
#import "SBRProcessPlugin.h"
#import "SBRWebProcessDelegate.h"
#import "SBRWebProcessProxy.h"
#import "SBRScriptPolicy.h"
#import <WebKit/_WKRemoteObjectInterface.h>
#import <WebKit/_WKRemoteObjectRegistry.h>
#import <WebKit/WKWebProcessPlugInBrowserContextController.h>
#import <WebKit/WKWebProcessPlugInBrowserContextControllerPrivate.h>
#import <WebKit/WKWebProcessPlugInFrame.h>
#import <WebKit/WKWebProcessPlugInLoadDelegate.h>
@interface SBRProcessPlugin () <WKWebProcessPlugInLoadDelegate, SBRWebProcessProxy>
@property (nonatomic, strong) id<SBRWebProcessDelegate> processDelegate;
@property (nonatomic, assign) BOOL allScriptsAllowed;
@property (nonatomic, strong) NSMutableSet<NSString *> *allowedResourceOrigins;
@property (nonatomic, strong) NSDictionary<NSString *, NSNumber *> *policyTypeByOrigin;
@end
@implementation SBRProcessPlugin
+ (void)initialize
{
[super initialize];
NSLog(@"SBRProcessPlugin Initialized");
}
#pragma mark <SBRWebProcessProxy>
- (void)hello
{
NSLog(@"SBRProcessPlugin: Helloooooo");
}
- (void)syncAllowedResourceOrigins:(NSArray<NSString *> *)allowedOrigins
{
_allowedResourceOrigins = [NSMutableSet setWithArray:allowedOrigins];
}
- (void)syncPolicyTypes:(NSDictionary<NSString *,NSNumber *> *)policyTypes
{
_policyTypeByOrigin = policyTypes;
}
- (void)setAllScriptsAllowed:(BOOL)allScriptsAllowed
{
_allScriptsAllowed = allScriptsAllowed;
}
#pragma mark <WKWebProcessPlugIn>
- (void)webProcessPlugIn:(WKWebProcessPlugInController *)plugInController initializeWithObject:(id)initializationObject
{
_allowedResourceOrigins = [[plugInController parameters] valueForKey:SBRGetAllowedOriginsKey()];
_allScriptsAllowed = [[[plugInController parameters] valueForKey:SBRGetAllScriptsAllowedKey()] boolValue];
_policyTypeByOrigin = [[plugInController parameters] valueForKey:SBRGetPolicyTypeByOriginKey()];
NSLog(@"SBRProcessPlugin: %lu origins allowed, all scripts allowed: %@ ", (unsigned long)_allowedResourceOrigins.count, _allScriptsAllowed ? @"YES" : @"NO");
}
- (void)webProcessPlugIn:(WKWebProcessPlugInController *)plugInController didCreateBrowserContextController:(WKWebProcessPlugInBrowserContextController *)browserContextController
{
_WKRemoteObjectInterface *proxyInterface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(SBRWebProcessProxy)];
[browserContextController._remoteObjectRegistry registerExportedObject:self interface:proxyInterface];
_WKRemoteObjectInterface *eventListenerInterface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(SBRWebProcessDelegate)];
self.processDelegate = [browserContextController._remoteObjectRegistry remoteObjectProxyWithInterface:eventListenerInterface];
browserContextController.loadDelegate = self;
[[self processDelegate] webProcessDidConnect];
}
#pragma mark <WKWebProcessPlugInLoadDelegate>
- (NSURLRequest *)webProcessPlugInBrowserContextController:(WKWebProcessPlugInBrowserContextController *)controller frame:(WKWebProcessPlugInFrame *)frame willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse
{
NSLog(@"SBRProcessPlugin: Sending request: %@", request);
if (_allowedResourceOrigins == nil) {
NSLog(@"Allowed resource origins should not be nil!!!!");
}
NSURL *requestURL = [request URL];
NSString *resourceOrigin = [requestURL host];
NSString *requestExtension = [requestURL pathExtension];
NSString *hostOrigin = [[[controller mainFrame] URL] host];
if (requestExtension.length > 0 && [requestExtension isEqualToString:@"js"]) {
NSNumber *policyType = [_policyTypeByOrigin objectForKey:hostOrigin];
NSLog(@"SBRProcessPlugin: Policy type for %@: %@", hostOrigin, policyType);
SBRScriptPolicy *policy = [[SBRScriptPolicy alloc] initWithSecurityOrigin:hostOrigin policyType:[policyType integerValue]];
if ([self allScriptsAllowed] || [policy allowsExternalJavaScriptResourceOrigin:resourceOrigin]) {
NSLog(@"SBRProcessPlugin: Policy allows script requestURL: %@", requestURL);
[[self processDelegate] webProcessDidAllowScriptWithOrigin:resourceOrigin];
} else {
NSLog(@"SBRProcessPlugin: Policy disallows script requestURL: %@", requestURL);
[[self processDelegate] webProcessDidBlockScriptWithOrigin:resourceOrigin];
request = nil;
}
}
return request;
}
@end
@@ -1,16 +0,0 @@
//
// SBRWebProcessDelegate.h
// SBrowser
//
// Created by James Magahern on 7/22/20.
//
#import <Foundation/Foundation.h>
@protocol SBRWebProcessDelegate <NSObject>
- (void)webProcessDidConnect;
- (void)webProcessDidBlockScriptWithOrigin:(NSString *)origin;
- (void)webProcessDidAllowScriptWithOrigin:(NSString *)origin;
@end
@@ -1,29 +0,0 @@
//
// SBRWebProcessProxy.h
// SBrowser
//
// Created by James Magahern on 7/22/20.
//
#import <Foundation/Foundation.h>
static inline NSString* SBRGetAllowedOriginsKey() {
return @"allowedOrigins";
}
static inline NSString* SBRGetAllScriptsAllowedKey() {
return @"allScriptsAllowed";
}
static inline NSString* SBRGetPolicyTypeByOriginKey() {
return @"policyTypeByOrigin";
}
@protocol SBRWebProcessProxy <NSObject>
- (void)hello;
- (void)setAllScriptsAllowed:(BOOL)allowed;
- (void)syncAllowedResourceOrigins:(NSArray<NSString *> *)allowedOrigins;
- (void)syncPolicyTypes:(NSDictionary<NSString *, NSNumber *> *)policyTypes;
@end
Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

+162
View File
@@ -0,0 +1,162 @@
/*
* Copyright (C) 2010-2017 Apple Inc. All rights reserved.
* Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef WKBase_h
#define WKBase_h
#include <WebKit/WKDeclarationSpecifiers.h>
#include <stdint.h>
#include <WebKit/WKBaseMac.h>
/* WebKit2 shared types */
typedef uint32_t WKTypeID;
typedef const void* WKTypeRef;
typedef const struct OpaqueWKArray* WKArrayRef;
typedef struct OpaqueWKArray* WKMutableArrayRef;
typedef const struct OpaqueWKDictionary* WKDictionaryRef;
typedef struct OpaqueWKDictionary* WKMutableDictionaryRef;
typedef const struct OpaqueWKBoolean* WKBooleanRef;
typedef const struct OpaqueWKCertificateInfo* WKCertificateInfoRef;
typedef const struct OpaqueWKConnection* WKConnectionRef;
typedef const struct OpaqueWKContextMenuItem* WKContextMenuItemRef;
typedef const struct OpaqueWKData* WKDataRef;
typedef const struct OpaqueWKDouble* WKDoubleRef;
typedef const struct OpaqueWKError* WKErrorRef;
typedef const struct OpaqueWKGraphicsContext* WKGraphicsContextRef;
typedef const struct OpaqueWKImage* WKImageRef;
typedef const struct OpaqueWKPointRef* WKPointRef;
typedef const struct OpaqueWKRectRef* WKRectRef;
typedef const struct OpaqueWKSecurityOrigin* WKSecurityOriginRef;
typedef const struct OpaqueWKSerializedScriptValue* WKSerializedScriptValueRef;
typedef const struct OpaqueWKSizeRef* WKSizeRef;
typedef const struct OpaqueWKString* WKStringRef;
typedef const struct OpaqueWKUInt64* WKUInt64Ref;
typedef const struct OpaqueWKURL* WKURLRef;
typedef const struct OpaqueWKURLRequest* WKURLRequestRef;
typedef const struct OpaqueWKURLResponse* WKURLResponseRef;
typedef const struct OpaqueWKUserContentURLPattern* WKUserContentURLPatternRef;
typedef const struct OpaqueWKWebArchive* WKWebArchiveRef;
typedef const struct OpaqueWKWebArchiveResource* WKWebArchiveResourceRef;
/* WebKit2 main API types */
typedef const struct OpaqueWKApplicationCacheManager* WKApplicationCacheManagerRef;
typedef const struct OpaqueWKAuthenticationChallenge* WKAuthenticationChallengeRef;
typedef const struct OpaqueWKAuthenticationDecisionListener* WKAuthenticationDecisionListenerRef;
typedef const struct OpaqueWKBackForwardList* WKBackForwardListRef;
typedef const struct OpaqueWKBackForwardListItem* WKBackForwardListItemRef;
typedef const struct OpaqueWKResourceCacheManager* WKResourceCacheManagerRef;
typedef const struct OpaqueWKColorPickerResultListener* WKColorPickerResultListenerRef;
typedef const struct OpaqueWKContext* WKContextRef;
typedef const struct OpaqueWKContextConfiguration* WKContextConfigurationRef;
typedef const struct OpaqueWKContextMenuListener* WKContextMenuListenerRef;
typedef const struct OpaqueWKCookieManager* WKCookieManagerRef;
typedef const struct OpaqueWKCredential* WKCredentialRef;
typedef const struct OpaqueWKDownload* WKDownloadRef;
typedef const struct OpaqueWKExperimentalFeature* WKFeatureRef;
typedef const struct OpaqueWKFormSubmissionListener* WKFormSubmissionListenerRef;
typedef const struct OpaqueWKFrameHandle* WKFrameHandleRef;
typedef const struct OpaqueWKFrameInfo* WKFrameInfoRef;
typedef const struct OpaqueWKFrame* WKFrameRef;
typedef const struct OpaqueWKFramePolicyListener* WKFramePolicyListenerRef;
typedef const struct OpaqueWKGeolocationManager* WKGeolocationManagerRef;
typedef const struct OpaqueWKGeolocationPermissionRequest* WKGeolocationPermissionRequestRef;
typedef const struct OpaqueWKGeolocationPosition* WKGeolocationPositionRef;
typedef const struct OpaqueWKGrammarDetail* WKGrammarDetailRef;
typedef const struct OpaqueWKHTTPCookieStore* WKHTTPCookieStoreRef;
typedef const struct OpaqueWKHitTestResult* WKHitTestResultRef;
typedef const struct OpaqueWKIconDatabase* WKIconDatabaseRef;
typedef const struct OpaqueWKInspector* WKInspectorRef;
typedef const struct OpaqueWKInternalDebugFeature* WKInternalDebugFeatureRef;
typedef const struct OpaqueWKKeyValueStorageManager* WKKeyValueStorageManagerRef;
typedef const struct OpaqueWKMessageListener* WKMessageListenerRef;
typedef const struct OpaqueWKNavigationAction* WKNavigationActionRef;
typedef const struct OpaqueWKNavigationData* WKNavigationDataRef;
typedef const struct OpaqueWKNavigation* WKNavigationRef;
typedef const struct OpaqueWKNavigationResponse* WKNavigationResponseRef;
typedef const struct OpaqueWKNotification* WKNotificationRef;
typedef const struct OpaqueWKNotificationManager* WKNotificationManagerRef;
typedef const struct OpaqueWKNotificationPermissionRequest* WKNotificationPermissionRequestRef;
typedef const struct OpaqueWKNotificationProvider* WKNotificationProviderRef;
typedef const struct OpaqueWKOpenPanelParameters* WKOpenPanelParametersRef;
typedef const struct OpaqueWKOpenPanelResultListener* WKOpenPanelResultListenerRef;
typedef const struct OpaqueWKPage* WKPageRef;
typedef const struct OpaqueWKPageConfiguration* WKPageConfigurationRef;
typedef const struct OpaqueWKPageGroup* WKPageGroupRef;
typedef const struct OpaqueWKPreferences* WKPreferencesRef;
typedef const struct OpaqueWKProtectionSpace* WKProtectionSpaceRef;
typedef const struct OpaqueWKPageRunBeforeUnloadConfirmPanelResultListener* WKPageRunBeforeUnloadConfirmPanelResultListenerRef;
typedef const struct OpaqueWKPageRunJavaScriptAlertResultListener* WKPageRunJavaScriptAlertResultListenerRef;
typedef const struct OpaqueWKPageRunJavaScriptConfirmResultListener* WKPageRunJavaScriptConfirmResultListenerRef;
typedef const struct OpaqueWKPageRunJavaScriptPromptResultListener* WKPageRunJavaScriptPromptResultListenerRef;
typedef const struct OpaqueWKPageRequestStorageAccessConfirmResultListener* WKPageRequestStorageAccessConfirmResultListenerRef;
typedef const struct OpaqueWKResourceLoadStatisticsManager* WKResourceLoadStatisticsManagerRef;
typedef const struct OpaqueWKTextChecker* WKTextCheckerRef;
typedef const struct OpaqueWKSession* WKSessionRef;
typedef const struct OpaqueWKSessionState* WKSessionStateRef;
typedef const struct OpaqueWKUserContentController* WKUserContentControllerRef;
typedef const struct OpaqueWKUserContentExtensionStore* WKUserContentExtensionStoreRef;
typedef const struct OpaqueWKUserContentFilter* WKUserContentFilterRef;
typedef const struct OpaqueWKUserMediaPermissionCheck* WKUserMediaPermissionCheckRef;
typedef const struct OpaqueWKUserMediaPermissionRequest* WKUserMediaPermissionRequestRef;
typedef const struct OpaqueWKUserScript* WKUserScriptRef;
typedef const struct OpaqueWKViewportAttributes* WKViewportAttributesRef;
typedef const struct OpaqueWKWebsiteDataConfigurationStore* WKWebsiteDataStoreConfigurationRef;
typedef const struct OpaqueWKWebsiteDataStore* WKWebsiteDataStoreRef;
typedef const struct OpaqueWKWebsitePolicies* WKWebsitePoliciesRef;
typedef const struct OpaqueWKWindowFeatures* WKWindowFeaturesRef;
typedef const struct OpaqueWKSpeechRecognitionPermissionCallback* WKSpeechRecognitionPermissionCallbackRef;
typedef const struct OpaqueWKMediaKeySystemPermissionRequest* WKMediaKeySystemPermissionRequestRef;
typedef const struct OpaqueWKMediaKeySystemPermissionCallback* WKMediaKeySystemPermissionCallbackRef;
typedef const struct OpaqueWKQueryPermissionResultCallback* WKQueryPermissionResultCallbackRef;
/* WebKit2 Bundle types */
typedef const struct OpaqueWKBundle* WKBundleRef;
typedef const struct OpaqueWKBundleBackForwardList* WKBundleBackForwardListRef;
typedef const struct OpaqueWKBundleBackForwardListItem* WKBundleBackForwardListItemRef;
typedef const struct OpaqueWKBundleDOMCSSStyleDeclaration* WKBundleCSSStyleDeclarationRef;
typedef const struct OpaqueWKBundleDOMWindowExtension* WKBundleDOMWindowExtensionRef;
typedef const struct OpaqueWKBundleFileHandle* WKBundleFileHandleRef;
typedef const struct OpaqueWKBundleFrame* WKBundleFrameRef;
typedef const struct OpaqueWKBundleHitTestResult* WKBundleHitTestResultRef;
typedef const struct OpaqueWKBundleInspector* WKBundleInspectorRef;
typedef const struct OpaqueWKBundleNavigationAction* WKBundleNavigationActionRef;
typedef const struct OpaqueWKBundleNodeHandle* WKBundleNodeHandleRef;
typedef const struct OpaqueWKBundlePage* WKBundlePageRef;
typedef const struct OpaqueWKBundlePageBanner* WKBundlePageBannerRef;
typedef const struct OpaqueWKBundlePageGroup* WKBundlePageGroupRef;
typedef const struct OpaqueWKBundlePageOverlay* WKBundlePageOverlayRef;
typedef const struct OpaqueWKBundleRangeHandle* WKBundleRangeHandleRef;
typedef const struct OpaqueWKBundleScriptWorld* WKBundleScriptWorldRef;
#endif /* WKBase_h */
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2013 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef WKBaseMac_h
#define WKBaseMac_h
#ifndef WKBase_h
#error "Please #include \"WKBase.h\" instead of this file directly."
#endif
typedef const struct OpaqueWKObjCTypeWrapper* WKObjCTypeWrapperRef;
#endif /* WKBaseMac_h */
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2010, 2011 Apple Inc. All rights reserved.
* Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef WKDeclarationSpecifiers_h
#define WKDeclarationSpecifiers_h
#ifndef __has_declspec_attribute
#define __has_declspec_attribute(x) 0
#endif
#undef WK_EXPORT
#if defined(WK_NO_EXPORT)
#define WK_EXPORT
#elif defined(WIN32) || defined(_WIN32) || defined(__CC_ARM) || defined(__ARMCC__) || (__has_declspec_attribute(dllimport) && __has_declspec_attribute(dllexport))
#if defined(BUILDING_WebKit)
#define WK_EXPORT __declspec(dllexport)
#else
#define WK_EXPORT __declspec(dllimport)
#endif /* defined(BUILDING_WebKit) */
#elif defined(__GNUC__)
#define WK_EXPORT __attribute__((visibility("default")))
#else /* !defined(WK_NO_EXPORT) */
#define WK_EXPORT
#endif /* defined(WK_NO_EXPORT) */
#if !defined(WK_INLINE)
#if defined(__cplusplus)
#define WK_INLINE static inline
#elif defined(__GNUC__)
#define WK_INLINE static __inline__
#else
#define WK_INLINE static
#endif
#endif /* !defined(WK_INLINE) */
#ifndef __has_extension
#define __has_extension(x) 0
#endif
#if __has_extension(enumerator_attributes) && __has_extension(attribute_unavailable_with_message)
#define WK_C_DEPRECATED(message) __attribute__((deprecated(message)))
#else
#define WK_C_DEPRECATED(message)
#endif
#ifndef __has_attribute
#define __has_attribute(x) 0
#endif
#if __has_attribute(unavailable)
#define WK_UNAVAILABLE __attribute__((unavailable))
#else
#define WK_UNAVAILABLE
#endif
#endif /* WKDeclarationSpecifiers_h */
+47
View File
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef WKLayoutMode_h
#define WKLayoutMode_h
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
kWKLayoutModeViewSize = 0,
kWKLayoutModeFixedSize = 1,
kWKLayoutModeDynamicSizeComputedFromViewScale = 2,
kWKLayoutModeDynamicSizeComputedFromMinimumDocumentSize = 4
};
typedef uint32_t WKLayoutMode;
#ifdef __cplusplus
}
#endif
#endif /* WKLayoutMode_h */
@@ -0,0 +1,149 @@
/*
* Copyright (C) 2014-2022 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <WebKit/WKBase.h>
#import <WebKit/WKProcessPool.h>
#import <WebKit/WKSecurityOrigin.h>
@class WKWebView;
@class WKWebsiteDataStore;
@class _WKAutomationSession;
@class _WKDownload;
@class _WKProcessPoolConfiguration;
@protocol _WKAutomationDelegate;
@protocol _WKDownloadDelegate;
@protocol _WKGeolocationCoreLocationProvider;
@interface WKProcessPool ()
- (instancetype)_initWithConfiguration:(_WKProcessPoolConfiguration *)configuration __attribute__((objc_method_family(init))) NS_DESIGNATED_INITIALIZER;
@end
@interface WKProcessPool (WKPrivate)
+ (WKProcessPool *)_sharedProcessPool;
+ (NSArray<WKProcessPool *> *)_allProcessPoolsForTesting WK_API_AVAILABLE(macos(10.14), ios(12.0));
@property (nonatomic, readonly) _WKProcessPoolConfiguration *_configuration;
- (void)_setAllowsSpecificHTTPSCertificate:(NSArray *)certificateChain forHost:(NSString *)host WK_API_DEPRECATED_WITH_REPLACEMENT("WKWebsiteDataStore._allowTLSCertificateChain:forHost:", macos(10.10, 12.0), ios(8.0, 15.0));
- (void)_setCanHandleHTTPSServerTrustEvaluation:(BOOL)value WK_API_DEPRECATED_WITH_REPLACEMENT("_WKWebsiteDataStoreConfiguration.fastServerTrustEvaluationEnabled", macos(10.11, 10.15.4), ios(9.0, 13.4));
- (id)_objectForBundleParameter:(NSString *)parameter;
- (void)_setObject:(id <NSCopying, NSSecureCoding>)object forBundleParameter:(NSString *)parameter;
// FIXME: This should be NSDictionary<NSString *, id <NSCopying, NSSecureCoding>>
- (void)_setObjectsForBundleParametersWithDictionary:(NSDictionary *)dictionary WK_API_AVAILABLE(macos(10.12), ios(10.0));
#if !TARGET_OS_IPHONE
- (void)_resetPluginLoadClientPolicies:(NSDictionary *)policies WK_API_AVAILABLE(macos(10.13));
@property (nonatomic, readonly, copy) NSDictionary *_pluginLoadClientPolicies WK_API_AVAILABLE(macos(10.13));
#endif
@property (nonatomic, weak, setter=_setDownloadDelegate:) id <_WKDownloadDelegate> _downloadDelegate WK_API_DEPRECATED_WITH_REPLACEMENT("WKDownload.downloadDelegate", macos(10.10, 12.0), ios(8.0, 15.0));
@property (nonatomic, weak, setter=_setAutomationDelegate:) id <_WKAutomationDelegate> _automationDelegate WK_API_AVAILABLE(macos(10.12), ios(10.0));
#if TARGET_OS_IPHONE
@property (nonatomic, setter=_setCoreLocationProvider:) id <_WKGeolocationCoreLocationProvider> _coreLocationProvider WK_API_AVAILABLE(ios(11.0));
#endif
+ (NSURL *)_websiteDataURLForContainerWithURL:(NSURL *)containerURL;
+ (NSURL *)_websiteDataURLForContainerWithURL:(NSURL *)containerURL bundleIdentifierIfNotInContainer:(NSString *)bundleIdentifier;
+ (void)_setWebProcessCountLimit:(unsigned)limit WK_API_AVAILABLE(macos(13.0), ios(16.0));
- (void)_warmInitialProcess WK_API_AVAILABLE(macos(10.12), ios(10.0));
- (void)_automationCapabilitiesDidChange WK_API_AVAILABLE(macos(10.12), ios(10.0));
- (void)_setAutomationSession:(_WKAutomationSession *)automationSession WK_API_AVAILABLE(macos(10.12), ios(10.0));
@property (nonatomic, copy, setter=_setJavaScriptConfigurationDirectory:) NSURL *_javaScriptConfigurationDirectory WK_API_AVAILABLE(macos(10.15.4), ios(13.4));
- (void)_addSupportedPlugin:(NSString *) domain named:(NSString *) name withMimeTypes: (NSSet<NSString *> *) mimeTypes withExtensions: (NSSet<NSString *> *) extensions WK_API_AVAILABLE(macos(10.14), ios(12.0));
- (void)_clearSupportedPlugins WK_API_AVAILABLE(macos(10.14), ios(12.0));
- (void)_registerURLSchemeAsCanDisplayOnlyIfCanRequest:(NSString *)scheme WK_API_AVAILABLE(macos(10.14), ios(12.0));
- (void)_registerURLSchemeAsSecure:(NSString *)scheme WK_API_AVAILABLE(macos(12.0), ios(15.0));
- (void)_registerURLSchemeAsBypassingContentSecurityPolicy:(NSString *)scheme WK_API_AVAILABLE(macos(12.0), ios(15.0));
- (void)_setDomainRelaxationForbiddenForURLScheme:(NSString *)scheme WK_API_AVAILABLE(macos(12.0), ios(15.0));
- (_WKDownload *)_downloadURLRequest:(NSURLRequest *)request websiteDataStore:(WKWebsiteDataStore *)dataStore originatingWebView:(WKWebView *)webView WK_API_DEPRECATED_WITH_REPLACEMENT("WKWebView _downloadRequest", macos(10.10, 12.0), ios(8.0, 15.0));
- (_WKDownload *)_resumeDownloadFromData:(NSData *)resumeData websiteDataStore:(WKWebsiteDataStore *)dataStore path:(NSString *)path originatingWebView:(WKWebView *)webView WK_API_DEPRECATED_WITH_REPLACEMENT("WKWebView.resumeDownloadFromResumeData:completionHandler:", macos(10.10, 12.0), ios(8.0, 15.0));
+ (void)_setLinkedOnOrAfterEverything WK_API_AVAILABLE(macos(13.0), ios(16.0));
// Test only. Should be called only while no web content processes are running.
- (void)_terminateServiceWorkers WK_API_AVAILABLE(macos(10.14), ios(12.0));
// Test only.
- (pid_t)_prewarmedProcessIdentifier WK_API_AVAILABLE(macos(10.15), ios(13.0));
- (void)_terminateAllWebContentProcesses;
// Test only.
- (size_t)_webProcessCount WK_API_AVAILABLE(macos(10.13), ios(11.0));
- (BOOL)_hasPrewarmedWebProcess WK_API_AVAILABLE(macos(10.14.4), ios(12.2));
- (size_t)_webProcessCountIgnoringPrewarmed WK_API_AVAILABLE(macos(10.14), ios(12.0));
- (size_t)_webProcessCountIgnoringPrewarmedAndCached WK_API_AVAILABLE(macos(10.14.4), ios(12.2));
- (size_t)_pluginProcessCount WK_API_AVAILABLE(macos(10.13.4), ios(11.3));
- (size_t)_serviceWorkerProcessCount WK_API_AVAILABLE(macos(10.14), ios(12.0));
- (void)_makeNextWebProcessLaunchFailForTesting WK_API_AVAILABLE(macos(10.14), ios(12.0));
- (NSUInteger)_maximumSuspendedPageCount WK_API_AVAILABLE(macos(10.14.4), ios(12.2));
- (NSUInteger)_processCacheCapacity WK_API_AVAILABLE(macos(10.14.4), ios(12.2));
- (NSUInteger)_processCacheSize WK_API_AVAILABLE(macos(10.15), ios(13.0));
- (void)_clearWebProcessCache WK_API_AVAILABLE(macos(10.15.4), ios(13.4));
- (void)_setUseSeparateServiceWorkerProcess:(BOOL)forceServiceWorkerProcess WK_API_AVAILABLE(macos(10.15.4), ios(13.4));
- (pid_t)_gpuProcessIdentifier WK_API_AVAILABLE(macos(12.0), ios(15.0));
- (BOOL)_hasAudibleMediaActivity WK_API_AVAILABLE(macos(13.0), ios(16.0));
- (BOOL)_requestWebProcessTermination:(pid_t)pid WK_API_AVAILABLE(macos(12.0), ios(15.0));
// Test only. Returns web processes running web pages (does not include web processes running service workers)
- (size_t)_webPageContentProcessCount WK_API_AVAILABLE(macos(10.13.4), ios(11.3));
// Test only. Should be called before any web content processes are launched.
+ (void)_forceGameControllerFramework WK_API_AVAILABLE(macos(10.13), ios(11.0));
+ (void)_setLinkedOnOrAfterEverythingForTesting WK_API_AVAILABLE(macos(12.0), ios(15.0));
+ (void)_setLinkedOnOrBeforeEverythingForTesting WK_API_AVAILABLE(macos(12.0), ios(15.0));
+ (void)_setCaptivePortalModeEnabledGloballyForTesting:(BOOL)isEnabled WK_API_AVAILABLE(macos(13.0), ios(16.0));
+ (void)_clearCaptivePortalModeEnabledGloballyForTesting WK_API_AVAILABLE(macos(13.0), ios(16.0));
+ (BOOL)_lockdownModeEnabledGloballyForTesting WK_API_AVAILABLE(macos(13.0), ios(16.0));
- (void)_preconnectToServer:(NSURL *)serverURL WK_API_DEPRECATED_WITH_REPLACEMENT("WKWebView._preconnectToServer", macos(10.13.4, 10.15.4), ios(11.3, 13.4));
// Test only.
- (void)_getActivePagesOriginsInWebProcessForTesting:(pid_t)pid completionHandler:(void(^)(NSArray<NSString *> *))completionHandler WK_API_AVAILABLE(macos(10.14.4), ios(12.2));
- (void)_clearPermanentCredentialsForProtectionSpace:(NSURLProtectionSpace *)protectionSpace WK_API_AVAILABLE(macos(10.15), ios(13.0));
@property (nonatomic, getter=_isCookieStoragePartitioningEnabled, setter=_setCookieStoragePartitioningEnabled:) BOOL _cookieStoragePartitioningEnabled WK_API_DEPRECATED("Partitioned cookies are no longer supported", macos(10.12.3, 10.14.4), ios(10.3, 12.2));
- (WKNotificationManagerRef)_notificationManagerForTesting;
// Test only.
- (void)_seedResourceLoadStatisticsForTestingWithFirstParty:(NSURL *)firstPartyURL thirdParty:(NSURL *)thirdPartyURL shouldScheduleNotification:(BOOL)shouldScheduleNotification completionHandler:(void(^)(void))completionHandler WK_API_AVAILABLE(macos(10.15.4), ios(13.4));
- (void)_garbageCollectJavaScriptObjectsForTesting WK_API_AVAILABLE(macos(11.0), ios(14.0));
- (size_t)_numberOfConnectedGamepadsForTesting WK_API_AVAILABLE(macos(11.0), ios(14.0));
- (size_t)_numberOfConnectedHIDGamepadsForTesting WK_API_AVAILABLE(macos(11.0), ios(15.0));
- (size_t)_numberOfConnectedGameControllerFrameworkGamepadsForTesting WK_API_AVAILABLE(macos(11.0), ios(14.0));
- (void)_setUsesOnlyHIDGamepadProviderForTesting:(BOOL)usesHIDProvider WK_API_AVAILABLE(macos(11.0), ios(14.0));
@end
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <WebKit/WKUserContentController.h>
@class WKContentWorld;
@class WKUserScript;
@class _WKUserContentFilter;
@class _WKUserContentWorld;
@class _WKUserStyleSheet;
@interface WKUserContentController (WKPrivate)
- (void)_removeUserScript:(WKUserScript *)userScript WK_API_AVAILABLE(macos(10.12), ios(10.0));
- (void)_removeAllUserScriptsAssociatedWithContentWorld:(WKContentWorld *)contentWorld WK_API_AVAILABLE(macos(11.0), ios(14.0));
- (void)_addUserScriptImmediately:(WKUserScript *)userScript WK_API_AVAILABLE(macos(10.14), ios(12.0));
// FIXME: Remove this once rdar://100785999 is unblocked.
- (void)_addUserContentFilter:(_WKUserContentFilter *)userContentFilter WK_API_DEPRECATED_WITH_REPLACEMENT("addContentRuleList", macos(10.11, 13.3), ios(9.0, 16.4));
- (void)_removeUserContentFilter:(NSString *)userContentFilterName WK_API_AVAILABLE(macos(10.11), ios(9.0));
- (void)_removeAllUserContentFilters WK_API_AVAILABLE(macos(10.11), ios(9.0));
- (void)_addContentRuleList:(WKContentRuleList *)contentRuleList extensionBaseURL:(NSURL *)extensionBaseURL WK_API_AVAILABLE(macos(13.0), ios(16.0));
@property (nonatomic, readonly, copy) NSArray<_WKUserStyleSheet *> *_userStyleSheets WK_API_AVAILABLE(macos(10.12), ios(10.0));
- (void)_addUserStyleSheet:(_WKUserStyleSheet *)userStyleSheet WK_API_AVAILABLE(macos(10.12), ios(10.0));
- (void)_removeUserStyleSheet:(_WKUserStyleSheet *)userStyleSheet WK_API_AVAILABLE(macos(10.12), ios(10.0));
- (void)_removeAllUserStyleSheets WK_API_AVAILABLE(macos(10.12), ios(10.0));
- (void)_removeAllUserStyleSheetsAssociatedWithContentWorld:(WKContentWorld *)contentWorld WK_API_AVAILABLE(macos(11.0), ios(14.0));
- (void)_addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name userContentWorld:(_WKUserContentWorld *)userContentWorld WK_API_DEPRECATED_WITH_REPLACEMENT("_addScriptMessageHandler:name:contentWorld:", macos(10.11, 11.0), ios(9.0, 14.0));
- (void)_addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name contentWorld:(WKContentWorld *)contentWorld;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
- (void)_removeScriptMessageHandlerForName:(NSString *)name userContentWorld:(_WKUserContentWorld *)userContentWorld;
- (void)_removeAllScriptMessageHandlersAssociatedWithUserContentWorld:(_WKUserContentWorld *)userContentWorld;
#pragma clang diagnostic pop
@end

Some files were not shown because too many files have changed in this diff Show More