From 37eeeacc8534390a79fc0b370afa774991059a4f Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 24 Jul 2020 19:26:35 -0700 Subject: [PATCH] Script blocking UI works now --- SBrowser.xcodeproj/project.pbxproj | 44 +++++++ SBrowser/Backend/ResourcePolicyManager.swift | 8 +- SBrowser/Browser View/BrowserView.swift | 46 ++++++- .../Browser View/BrowserViewController.swift | 97 +++++++++++---- .../Browser View/ToolbarViewController.swift | 109 +++++++++++++++++ SBrowser/Browser View/URLBar.swift | 58 +++++++++ SBrowser/SceneDelegate.swift | 1 - .../ScriptControllerIconView.swift | 55 +++++++++ .../ScriptPolicyControl.swift | 73 +++++++++++ .../ScriptPolicyViewController.swift | 113 ++++++++++++++++++ .../SBrowser-Bridging-Header.h | 3 + SBrowser/Utilities/CGPoint+Utils.swift | 17 +++ SBrowser/Utilities/UIEdgeInsets+Layout.swift | 17 +++ .../SBRProcessBundleBridge.m | 2 +- SBrowserProcessBundle/SBRProcessPlugin.m | 8 +- SBrowserProcessBundle/SBRWebProcessProxy.h | 2 +- 16 files changed, 619 insertions(+), 34 deletions(-) create mode 100644 SBrowser/Browser View/ToolbarViewController.swift create mode 100644 SBrowser/Browser View/URLBar.swift create mode 100644 SBrowser/Script Policy UI/ScriptControllerIconView.swift create mode 100644 SBrowser/Script Policy UI/ScriptPolicyControl.swift create mode 100644 SBrowser/Script Policy UI/ScriptPolicyViewController.swift create mode 100644 SBrowser/Utilities/CGPoint+Utils.swift create mode 100644 SBrowser/Utilities/UIEdgeInsets+Layout.swift diff --git a/SBrowser.xcodeproj/project.pbxproj b/SBrowser.xcodeproj/project.pbxproj index 0c56f10..d4fd467 100644 --- a/SBrowser.xcodeproj/project.pbxproj +++ b/SBrowser.xcodeproj/project.pbxproj @@ -18,6 +18,13 @@ 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 /* CGPoint+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4C224CA6AF6006DC7AE /* CGPoint+Utils.swift */; }; + 1ADFF4C724CA6DEB006DC7AE /* UIEdgeInsets+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4C624CA6DEB006DC7AE /* UIEdgeInsets+Layout.swift */; }; + 1ADFF4C924CA793E006DC7AE /* ToolbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4C824CA793E006DC7AE /* ToolbarViewController.swift */; }; + 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CA24CB8278006DC7AE /* ScriptControllerIconView.swift */; }; + 1ADFF4CD24CBB0C8006DC7AE /* ScriptPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CC24CBB0C8006DC7AE /* ScriptPolicyViewController.swift */; }; + 1ADFF4D024CBBCD1006DC7AE /* ScriptPolicyControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CF24CBBCD1006DC7AE /* ScriptPolicyControl.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -65,6 +72,13 @@ 1ADFF4AB24C8DF62006DC7AE /* SBRWebProcessDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SBRWebProcessDelegate.h; sourceTree = ""; }; 1ADFF4AC24C8DFEE006DC7AE /* SBRWebProcessProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SBRWebProcessProxy.h; sourceTree = ""; }; 1ADFF4AD24C8ED32006DC7AE /* ResourcePolicyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcePolicyManager.swift; sourceTree = ""; }; + 1ADFF4BF24CA6964006DC7AE /* URLBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBar.swift; sourceTree = ""; }; + 1ADFF4C224CA6AF6006DC7AE /* CGPoint+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint+Utils.swift"; sourceTree = ""; }; + 1ADFF4C624CA6DEB006DC7AE /* UIEdgeInsets+Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Layout.swift"; sourceTree = ""; }; + 1ADFF4C824CA793E006DC7AE /* ToolbarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarViewController.swift; sourceTree = ""; }; + 1ADFF4CA24CB8278006DC7AE /* ScriptControllerIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptControllerIconView.swift; sourceTree = ""; }; + 1ADFF4CC24CBB0C8006DC7AE /* ScriptPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewController.swift; sourceTree = ""; }; + 1ADFF4CF24CBBCD1006DC7AE /* ScriptPolicyControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyControl.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -112,6 +126,8 @@ 1ADFF46124C7DE53006DC7AE /* SceneDelegate.swift */, 1ADFF47A24C7E176006DC7AE /* Backend */, 1ADFF47724C7DFE8006DC7AE /* Browser View */, + 1ADFF4CE24CBBCBD006DC7AE /* Script Policy UI */, + 1ADFF4C124CA6AE4006DC7AE /* Utilities */, 1ADFF4AF24C92E2F006DC7AE /* Web Process Bundle Bridge */, 1ADFF47624C7DF7F006DC7AE /* Supporting Files */, ); @@ -135,6 +151,8 @@ children = ( 1ADFF47324C7DE9C006DC7AE /* BrowserViewController.swift */, 1ADFF47824C7DFF8006DC7AE /* BrowserView.swift */, + 1ADFF4C824CA793E006DC7AE /* ToolbarViewController.swift */, + 1ADFF4BF24CA6964006DC7AE /* URLBar.swift */, ); path = "Browser View"; sourceTree = ""; @@ -176,6 +194,25 @@ path = "Web Process Bundle Bridge"; sourceTree = ""; }; + 1ADFF4C124CA6AE4006DC7AE /* Utilities */ = { + isa = PBXGroup; + children = ( + 1ADFF4C224CA6AF6006DC7AE /* CGPoint+Utils.swift */, + 1ADFF4C624CA6DEB006DC7AE /* UIEdgeInsets+Layout.swift */, + ); + path = Utilities; + sourceTree = ""; + }; + 1ADFF4CE24CBBCBD006DC7AE /* Script Policy UI */ = { + isa = PBXGroup; + children = ( + 1ADFF4CA24CB8278006DC7AE /* ScriptControllerIconView.swift */, + 1ADFF4CF24CBBCD1006DC7AE /* ScriptPolicyControl.swift */, + 1ADFF4CC24CBB0C8006DC7AE /* ScriptPolicyViewController.swift */, + ); + path = "Script Policy UI"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -277,10 +314,17 @@ buildActionMask = 2147483647; files = ( 1ADFF46024C7DE53006DC7AE /* AppDelegate.swift in Sources */, + 1ADFF4C024CA6964006DC7AE /* URLBar.swift in Sources */, + 1ADFF4C724CA6DEB006DC7AE /* UIEdgeInsets+Layout.swift in Sources */, 1ADFF4AE24C8ED32006DC7AE /* ResourcePolicyManager.swift in Sources */, 1ADFF47424C7DE9C006DC7AE /* BrowserViewController.swift in Sources */, + 1ADFF4D024CBBCD1006DC7AE /* ScriptPolicyControl.swift in Sources */, 1ADFF48D24C8C176006DC7AE /* SBRProcessBundleBridge.m in Sources */, 1ADFF46224C7DE53006DC7AE /* SceneDelegate.swift in Sources */, + 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */, + 1ADFF4C324CA6AF6006DC7AE /* CGPoint+Utils.swift in Sources */, + 1ADFF4C924CA793E006DC7AE /* ToolbarViewController.swift in Sources */, + 1ADFF4CD24CBB0C8006DC7AE /* ScriptPolicyViewController.swift in Sources */, 1ADFF47924C7DFF8006DC7AE /* BrowserView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SBrowser/Backend/ResourcePolicyManager.swift b/SBrowser/Backend/ResourcePolicyManager.swift index a78448d..ac22133 100644 --- a/SBrowser/Backend/ResourcePolicyManager.swift +++ b/SBrowser/Backend/ResourcePolicyManager.swift @@ -26,6 +26,12 @@ class ResourcePolicyManager: NSObject, SBRResourceOriginPolicyDataSource func allowOriginToLoadScriptResources(_ origin: String) { allowedOriginSet.formUnion([ origin ]) - UserDefaults.standard.set(allowedOriginSet, forKey: Self.AllowedOriginsDefaultsKey) + UserDefaults.standard.set(Array(allowedOriginSet), forKey: Self.AllowedOriginsDefaultsKey) + } + + func disallowOriginToLoadScriptResources(_ origin: String) + { + allowedOriginSet.remove(origin) + UserDefaults.standard.set(Array(allowedOriginSet), forKey: Self.AllowedOriginsDefaultsKey) } } diff --git a/SBrowser/Browser View/BrowserView.swift b/SBrowser/Browser View/BrowserView.swift index 2fde612..cfa6db3 100644 --- a/SBrowser/Browser View/BrowserView.swift +++ b/SBrowser/Browser View/BrowserView.swift @@ -5,19 +5,59 @@ // Created by James Magahern on 7/21/20. // +import Combine import UIKit import WebKit class BrowserView: UIView { - var webView: WKWebView? { - didSet { addSubview(webView!); setNeedsLayout() } + var toolbarView: ToolbarView? { + didSet { addSubview(toolbarView!) } } + + var webView: WKWebView? { + didSet { + if let toolbarView = toolbarView { + insertSubview(webView!, belowSubview: toolbarView) + } else { + addSubview(webView!) + } + } + } + + var keyboardWillShowObserver: AnyCancellable? + var keyboardWillHideObserver: AnyCancellable? + var keyboardLayoutOffset: CGFloat = 0 { didSet { setNeedsLayout() } } + + convenience init() { + self.init(frame: .zero) + keyboardWillShowObserver = NotificationCenter.default.publisher(for: UIWindow.keyboardWillShowNotification).sink { notification in + if let keyboardFrame = notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect { + self.keyboardLayoutOffset = self.bounds.height - keyboardFrame.minY + } + } + + keyboardWillHideObserver = NotificationCenter.default.publisher(for: UIWindow.keyboardWillHideNotification).sink { notification in + if let keyboardFrame = notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect { + self.keyboardLayoutOffset = self.bounds.height - keyboardFrame.minY + } + } + } + override func layoutSubviews() { super.layoutSubviews() webView?.frame = bounds + + if let toolbarView = toolbarView { + var toolbarSize = toolbarView.sizeThatFits(bounds.size) + if keyboardLayoutOffset == 0 { + toolbarSize.height += safeAreaInsets.bottom + } + + toolbarView.bounds = CGRect(origin: .zero, size: toolbarSize) + toolbarView.center = CGPoint(x: bounds.center.x, y: bounds.maxY - (toolbarView.bounds.height / 2) - keyboardLayoutOffset) + } } - } diff --git a/SBrowser/Browser View/BrowserViewController.swift b/SBrowser/Browser View/BrowserViewController.swift index a8efc38..e2ea989 100644 --- a/SBrowser/Browser View/BrowserViewController.swift +++ b/SBrowser/Browser View/BrowserViewController.swift @@ -7,59 +7,114 @@ import UIKit -class BrowserViewController: UIViewController, SBRProcessBundleBridgeDelegate +class BrowserViewController: UIViewController, + SBRProcessBundleBridgeDelegate, WKNavigationDelegate, + UITextFieldDelegate, ScriptPolicyViewControllerDelegate { let bridge = SBRProcessBundleBridge() let browserView = BrowserView() private let policyManager = ResourcePolicyManager() + private let toolbarController = ToolbarViewController() private var blockedScriptOrigins = Set() - private var scriptBlockerButtonItem: UIBarButtonItem + override var canBecomeFirstResponder: Bool { true } - init() - { - scriptBlockerButtonItem = UIBarButtonItem(title: "0", image: nil, primaryAction: UIAction(handler: { action in - // present - }), menu: nil) - + init() { super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func loadView() - { + override func loadView() { bridge.delegate = self bridge.policyDataSource = policyManager let webView = bridge.webView webView.allowsBackForwardNavigationGestures = true + webView.navigationDelegate = self browserView.webView = webView + browserView.toolbarView = toolbarController.toolbarView + + // Refresh button + toolbarController.urlBar.refreshButton.addAction(UIAction(handler: { action in + webView.reload() + }), for: .touchUpInside) + + // Script button + toolbarController.scriptControllerIconView.addAction(UIAction(handler: { action in + let scriptViewController = ScriptPolicyViewController(policyManager: self.policyManager, blockedScripts: self.blockedScriptOrigins) + scriptViewController.delegate = self + + let navController = UINavigationController(rootViewController: scriptViewController) + self.present(navController, animated: true, completion: nil) + }), for: .touchUpInside) + + // TextField delegate + toolbarController.urlBar.textField.delegate = self self.view = browserView } - override func viewDidLoad() - { - let request = URLRequest(url: URL(string: "https://yahoo.com")!) - browserView.webView?.load(request) - - setToolbarItems([ scriptBlockerButtonItem ], animated: false) + override func viewDidLoad() { + beginLoadingURL(URL(string: "https://reddit.com")!) } - private func updateScriptBlockerButton() - { - scriptBlockerButtonItem.title = "\(blockedScriptOrigins.count)" + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + becomeFirstResponder() + } + + private func updateScriptBlockerButton() { + toolbarController.scriptControllerIconView.setBlockedScriptsNumber(blockedScriptOrigins.count) + } + + func beginLoadingURL(_ url: URL) { + let request = URLRequest(url: url) + bridge.webView.load(request) } // MARK: SBRProcessBundleBridgeDelegate - func webProcess(_ bridge: SBRProcessBundleBridge, didBlockScriptResourceFromOrigin origin: String) - { + func webProcess(_ bridge: SBRProcessBundleBridge, didBlockScriptResourceFromOrigin origin: String) { print("Blocked script resource from origin: \(origin)") blockedScriptOrigins.formUnion([ origin ]) updateScriptBlockerButton() } + + // MARK: Navigation Delegate + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + // Reset tracking this + blockedScriptOrigins.removeAll() + + if let urlString = webView.url?.absoluteString { + toolbarController.urlBar.textField.text = urlString + } + } + + // MARK: UITextField Delegate + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if let text = textField.text, let url = URL(string: text) { + if url.scheme == nil { + let urlString = "https://\(text)" + if let url = URL(string: urlString) { + beginLoadingURL(url) + } + } + } + + textField.resignFirstResponder() + return false + } + + // MARK: Script Policy View Controller Delegate + + func didChangeScriptPolicy() { + bridge.policyDataSourceDidChange() + bridge.webView.reload() + } + } diff --git a/SBrowser/Browser View/ToolbarViewController.swift b/SBrowser/Browser View/ToolbarViewController.swift new file mode 100644 index 0000000..34a9b90 --- /dev/null +++ b/SBrowser/Browser View/ToolbarViewController.swift @@ -0,0 +1,109 @@ +// +// ToolbarViewController.swift +// SBrowser +// +// Created by James Magahern on 7/23/20. +// + +import UIKit + +class ToolbarButtonView: UIView +{ + private var buttonPadding = CGFloat(8.0) + private var buttonViews: [UIView] = [] + + func addButtonView(_ button: UIView) { + buttonViews.append(button) + addSubview(button) + setNeedsLayout() + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + let width: CGFloat = buttonViews.reduce(0.0) { (result, button) -> CGFloat in + return result + button.sizeThatFits(size).width + buttonPadding + } + + return CGSize(width: width, height: size.height) + } + + override func layoutSubviews() { + var buttonRect = CGRect(origin: .zero, size: CGSize(width: 0, height: bounds.height)) + buttonRect.origin.x = buttonPadding + + for button in buttonViews { + let buttonSize = button.sizeThatFits(bounds.size) + buttonRect.size = CGSize(width: buttonSize.width, height: bounds.height) + button.frame = buttonRect + + buttonRect.origin.x += buttonRect.width + buttonPadding + } + } +} + +class ToolbarView: UIView +{ + var urlBar: URLBar? { didSet { containerView.addSubview(urlBar!) } } + + let containerView = UIView(frame: .zero) + let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial)) + let buttonsView = ToolbarButtonView(frame: .zero) + + convenience init() + { + self.init(frame: .zero) + addSubview(backgroundView) + addSubview(containerView) + + containerView.addSubview(buttonsView) + } + + override func sizeThatFits(_ size: CGSize) -> CGSize + { + return CGSize(width: size.width, height: 44.0) + } + + override func layoutSubviews() + { + super.layoutSubviews() + + backgroundView.frame = bounds + + var containerBounds = bounds + containerBounds.size.height -= safeAreaInsets.bottom + containerView.frame = containerBounds + containerView.frame = containerView.frame.insetBy(dx: 8.0, dy: 4.0) + + let toolbarSize = buttonsView.sizeThatFits(containerView.bounds.size) + if let urlBar = urlBar { + urlBar.frame = CGRect(origin: .zero, size: CGSize(width: containerView.bounds.width - toolbarSize.width, height: toolbarSize.height)) + } + + buttonsView.frame = CGRect(origin: CGPoint(x: urlBar?.frame.maxX ?? 0 + 8.0, y: 0), size: toolbarSize) + } +} + +class ToolbarViewController: UIViewController +{ + let urlBar = URLBar() + let toolbarView = ToolbarView() + let scriptControllerIconView = ScriptControllerIconView() + let shareButton = UIButton(frame: .zero) + + init() { + super.init(nibName: nil, bundle: nil) + + toolbarView.urlBar = urlBar + + shareButton.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal) + toolbarView.buttonsView.addButtonView(shareButton) + toolbarView.buttonsView.addButtonView(scriptControllerIconView) + } + + override func loadView() { + self.view = toolbarView + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/SBrowser/Browser View/URLBar.swift b/SBrowser/Browser View/URLBar.swift new file mode 100644 index 0000000..1059bf4 --- /dev/null +++ b/SBrowser/Browser View/URLBar.swift @@ -0,0 +1,58 @@ +// +// URLBar.swift +// SBrowser +// +// Created by James Magahern on 7/23/20. +// + +import UIKit + +class URLBar: UIView +{ + let textField = UITextField(frame: .zero) + let refreshButton = UIButton(frame: .zero) + + private let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial)) + + convenience init() { + self.init(frame: .zero) + + backgroundColor = .clear + + backgroundView.layer.masksToBounds = true + backgroundView.layer.cornerRadius = 8 + backgroundView.layer.borderWidth = 1 + backgroundView.layer.borderColor = UIColor.systemFill.cgColor + backgroundView.isUserInteractionEnabled = false + addSubview(backgroundView) + + textField.backgroundColor = .clear + textField.textContentType = .URL + textField.keyboardType = .webSearch + textField.autocorrectionType = .no + textField.autocapitalizationType = .none + textField.font = .preferredFont(forTextStyle: .body) + textField.clearingBehavior = .clearOnInsertionAndShowSelectionTint + addSubview(textField) + + refreshButton.tintColor = .secondaryLabel + refreshButton.setImage(UIImage(systemName: "arrow.clockwise"), for: .normal) + addSubview(refreshButton) + } + + override var intrinsicContentSize: CGSize + { + let preferredHeight = CGFloat(34) + return CGSize(width: 1000.0, height: preferredHeight) + } + + override func layoutSubviews() + { + super.layoutSubviews() + backgroundView.frame = bounds + textField.frame = bounds.insetBy(dx: 6.0, dy: 0) + + 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) + } +} diff --git a/SBrowser/SceneDelegate.swift b/SBrowser/SceneDelegate.swift index 88f3f23..6b11f35 100644 --- a/SBrowser/SceneDelegate.swift +++ b/SBrowser/SceneDelegate.swift @@ -18,7 +18,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { navigationController.viewControllers = [ browserViewController ] navigationController.setNavigationBarHidden(true, animated: false) - navigationController.setToolbarHidden(false, animated: false) let window = UIWindow(windowScene: windowScene) window.rootViewController = navigationController diff --git a/SBrowser/Script Policy UI/ScriptControllerIconView.swift b/SBrowser/Script Policy UI/ScriptControllerIconView.swift new file mode 100644 index 0000000..beaf7fc --- /dev/null +++ b/SBrowser/Script Policy UI/ScriptControllerIconView.swift @@ -0,0 +1,55 @@ +// +// ScriptControllerIconView.swift +// SBrowser +// +// Created by James Magahern on 7/24/20. +// + +import UIKit + +class ScriptControllerIconView: UIButton +{ + private let labelView = UILabel(frame: .zero) + + convenience init() { + self.init(frame: .zero) + + addSubview(labelView) + + let image = UIImage(systemName: "shield") + setImage(image, for: .normal) + + imageView?.contentMode = .scaleAspectFit + labelView.backgroundColor = .systemRed + labelView.textAlignment = .center + labelView.layer.cornerRadius = 4.0 + labelView.layer.masksToBounds = true + labelView.font = .boldSystemFont(ofSize: 8) + labelView.textColor = .white + + setBlockedScriptsNumber(0) + } + + public func setBlockedScriptsNumber(_ num: Int) { + if num > 0 { + labelView.isHidden = false + labelView.text = "\(num)" + } else { + labelView.isHidden = true + } + + setNeedsLayout() + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + return CGSize(width: 44.0, height: 44.0) + } + + override func layoutSubviews() { + super.layoutSubviews() + + labelView.sizeToFit() + labelView.center = CGPoint(x: bounds.center.x + 10, y: bounds.center.y + 10) + labelView.bounds = labelView.bounds.insetBy(dx: -2.0, dy: -2.0) + } +} diff --git a/SBrowser/Script Policy UI/ScriptPolicyControl.swift b/SBrowser/Script Policy UI/ScriptPolicyControl.swift new file mode 100644 index 0000000..183b8a7 --- /dev/null +++ b/SBrowser/Script Policy UI/ScriptPolicyControl.swift @@ -0,0 +1,73 @@ +// +// ScriptPolicyControl.swift +// SBrowser +// +// Created by James Magahern on 7/24/20. +// + +import UIKit + +class ScriptPolicyControl: UIControl +{ + enum PolicyStatus { + case allowed + case blocked + } + + var policyStatus: PolicyStatus = .blocked { + didSet { + sendActions(for: .valueChanged) + setNeedsLayout() + } + } + + private class PolicyButton: UIButton { + override func imageRect(forContentRect contentRect: CGRect) -> CGRect { + contentRect.insetBy(dx: 8.0, dy: 8.0) + } + } + + private let allowButton = PolicyButton(frame: .zero) + private let denyButton = PolicyButton(frame: .zero) + + convenience init() { + self.init(frame: .zero) + + allowButton.addAction(UIAction(handler: { _ in + self.policyStatus = .allowed + }), for: .touchUpInside) + allowButton.imageView?.contentMode = .scaleAspectFit + addSubview(allowButton) + + denyButton.addAction(UIAction(handler: { _ in + self.policyStatus = .blocked + }), for: .touchUpInside) + denyButton.imageView?.contentMode = .scaleAspectFit + addSubview(denyButton) + } + + override var intrinsicContentSize: CGSize { + CGSize(width: 100.0, height: UIView.noIntrinsicMetric) + } + + override func layoutSubviews() { + super.layoutSubviews() + + allowButton.frame = CGRect(origin: .zero, size: CGSize(width: bounds.width / 2, height: bounds.height)) + denyButton.frame = CGRect(origin: CGPoint(x: allowButton.frame.maxX, y: 0), size: allowButton.frame.size) + + if policyStatus == .allowed { + allowButton.tintColor = .blue + allowButton.setImage(UIImage(systemName: "play.circle.fill"), for: .normal) + + denyButton.tintColor = .darkGray + denyButton.setImage(UIImage(systemName: "stop.circle"), for: .normal) + } else { + allowButton.tintColor = .darkGray + allowButton.setImage(UIImage(systemName: "play.circle"), for: .normal) + + denyButton.tintColor = .red + denyButton.setImage(UIImage(systemName: "stop.circle.fill"), for: .normal) + } + } +} diff --git a/SBrowser/Script Policy UI/ScriptPolicyViewController.swift b/SBrowser/Script Policy UI/ScriptPolicyViewController.swift new file mode 100644 index 0000000..bff3380 --- /dev/null +++ b/SBrowser/Script Policy UI/ScriptPolicyViewController.swift @@ -0,0 +1,113 @@ +// +// ScriptPolicyViewController.swift +// SBrowser +// +// Created by James Magahern on 7/24/20. +// + +import UIKit + +protocol ScriptPolicyViewControllerDelegate { + func didChangeScriptPolicy() +} + +class ScriptPolicyControlListCell: UICollectionViewListCell +{ + let policyControl = ScriptPolicyControl() + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(policyControl) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + let policyControlWidth = CGFloat(100.0) + policyControl.frame = CGRect(x: bounds.maxX - policyControlWidth, y: 0, width: policyControlWidth, height: bounds.height) + bringSubviewToFront(policyControl) + + contentView.frame = CGRect(origin: contentView.frame.origin, size: CGSize(width: bounds.width - policyControl.frame.width, height: contentView.frame.height)) + } +} + +class ScriptPolicyViewController: UIViewController, UICollectionViewDelegate +{ + var collectionView: UICollectionView? + var delegate: ScriptPolicyViewControllerDelegate? = nil + var dataSource: UICollectionViewDiffableDataSource? + + private var didChangeScriptPolicy = false + + convenience init(policyManager: ResourcePolicyManager, blockedScripts: Set) { + self.init(nibName: nil, bundle: nil) + + let listConfig = UICollectionLayoutListConfiguration(appearance: .grouped) + let listLayout = UICollectionViewCompositionalLayout.list(using: listConfig) + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: listLayout) + + let registry = UICollectionView.CellRegistration { (listCell, indexPath, item) in + var config = listCell.defaultContentConfiguration() + config.text = item + + listCell.contentConfiguration = config + + if policyManager.allowedOriginsForScriptResources().contains(item) { + listCell.policyControl.policyStatus = .allowed + } else { + listCell.policyControl.policyStatus = .blocked + } + + listCell.policyControl.addAction(UIAction(handler: { _ in + if listCell.policyControl.policyStatus == .allowed { + policyManager.allowOriginToLoadScriptResources(item) + } else { + policyManager.disallowOriginToLoadScriptResources(item) + } + + self.didChangeScriptPolicy = true + }), for: .valueChanged) + } + + let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, item) -> UICollectionViewCell? in + collectionView.dequeueConfiguredReusableCell(using: registry, for: indexPath, item: item) + } + + collectionView.dataSource = dataSource + collectionView.delegate = self + + var snapshot = dataSource.snapshot() + snapshot.appendSections([ 0 ]) + snapshot.appendItems(Array(blockedScripts)) + dataSource.apply(snapshot) + + self.dataSource = dataSource + self.collectionView = collectionView + + title = "Script Origin Policy" + navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction(handler: { action in + if self.didChangeScriptPolicy { + self.delegate?.didChangeScriptPolicy() + } + + self.dismiss(animated: true, completion: nil) + }), menu: nil) + } + + override func loadView() { + self.view = collectionView + } + + // MARK: UICollectionViewDelegate + + func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { + false + } + + func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { + false + } +} diff --git a/SBrowser/Supporting Files/SBrowser-Bridging-Header.h b/SBrowser/Supporting Files/SBrowser-Bridging-Header.h index 77ca5ef..8b4d486 100644 --- a/SBrowser/Supporting Files/SBrowser-Bridging-Header.h +++ b/SBrowser/Supporting Files/SBrowser-Bridging-Header.h @@ -3,3 +3,6 @@ // #import "SBRProcessBundleBridge.h" + +// SPI +#import diff --git a/SBrowser/Utilities/CGPoint+Utils.swift b/SBrowser/Utilities/CGPoint+Utils.swift new file mode 100644 index 0000000..13bd96d --- /dev/null +++ b/SBrowser/Utilities/CGPoint+Utils.swift @@ -0,0 +1,17 @@ +// +// CGPoint+Utils.swift +// SBrowser +// +// Created by James Magahern on 7/23/20. +// + +import Foundation + +extension CGRect +{ + var center: CGPoint { + get { + return CGPoint(x: size.width / 2.0, y: size.height / 2.0) + } + } +} diff --git a/SBrowser/Utilities/UIEdgeInsets+Layout.swift b/SBrowser/Utilities/UIEdgeInsets+Layout.swift new file mode 100644 index 0000000..7712c95 --- /dev/null +++ b/SBrowser/Utilities/UIEdgeInsets+Layout.swift @@ -0,0 +1,17 @@ +// +// UIEdgeInsets+Layout.swift +// SBrowser +// +// Created by James Magahern on 7/23/20. +// + +import UIKit + +extension UIEdgeInsets +{ + var negative: UIEdgeInsets { + get { + return UIEdgeInsets(top: -top, left: -left, bottom: -bottom, right: -right) + } + } +} diff --git a/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.m b/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.m index bd68ea5..718a58c 100644 --- a/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.m +++ b/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.m @@ -75,7 +75,7 @@ - (void)policyDataSourceDidChange { - NSSet *allowedOrigins = [_policyDataSource allowedOriginsForScriptResources]; + NSArray *allowedOrigins = [[_policyDataSource allowedOriginsForScriptResources] allObjects]; [_webProcessProxy syncAllowedResourceOrigins:allowedOrigins]; } diff --git a/SBrowserProcessBundle/SBRProcessPlugin.m b/SBrowserProcessBundle/SBRProcessPlugin.m index f3ecd85..994e601 100644 --- a/SBrowserProcessBundle/SBRProcessPlugin.m +++ b/SBrowserProcessBundle/SBRProcessPlugin.m @@ -36,13 +36,9 @@ NSLog(@"SBRProcessPlugin: Helloooooo"); } -- (void)syncAllowedResourceOrigins:(NSSet *)allowedOrigins +- (void)syncAllowedResourceOrigins:(NSArray *)allowedOrigins { - if (!_allowedResourceOrigins) { - _allowedResourceOrigins = [allowedOrigins mutableCopy]; - } else { - [_allowedResourceOrigins unionSet:allowedOrigins]; - } + _allowedResourceOrigins = [NSMutableSet setWithArray:allowedOrigins]; } #pragma mark diff --git a/SBrowserProcessBundle/SBRWebProcessProxy.h b/SBrowserProcessBundle/SBRWebProcessProxy.h index ac0c0ab..c689044 100644 --- a/SBrowserProcessBundle/SBRWebProcessProxy.h +++ b/SBrowserProcessBundle/SBRWebProcessProxy.h @@ -10,6 +10,6 @@ @protocol SBRWebProcessProxy - (void)hello; -- (void)syncAllowedResourceOrigins:(NSSet *)allowedOrigins; +- (void)syncAllowedResourceOrigins:(NSArray *)allowedOrigins; @end