From 0be50fff3e20e547f616cc02ddfac217252ee245 Mon Sep 17 00:00:00 2001 From: Josh Puetz Date: Thu, 3 Feb 2022 10:34:55 -0600 Subject: [PATCH 1/3] Add URL extension to switch preferredContentMode when going off to Google or FaceBook OAuth --- .../ForemWebView+WKNavigationDelegate.swift | 14 ++++++++++---- Sources/ForemWebView/URL+ForemUtilities.swift | 9 +++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 Sources/ForemWebView/URL+ForemUtilities.swift diff --git a/Sources/ForemWebView/ForemWebView+WKNavigationDelegate.swift b/Sources/ForemWebView/ForemWebView+WKNavigationDelegate.swift index 174b30b..30abc36 100644 --- a/Sources/ForemWebView/ForemWebView+WKNavigationDelegate.swift +++ b/Sources/ForemWebView/ForemWebView+WKNavigationDelegate.swift @@ -30,22 +30,28 @@ extension ForemWebView: WKNavigationDelegate { public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void) { + preferences: WKWebpagePreferences, + decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Swift.Void) { guard let url = navigationAction.request.url else { - decisionHandler(.allow) + decisionHandler(.allow, preferences) return } let policy = navigationPolicy(url: url, navigationType: navigationAction.navigationType) + //If we're going off to select OAuth providers, pop us into Desktop mode so we pass user agent checks + if url.isGoogleAuth || url.isFacebookAuth { + preferences.preferredContentMode = .desktop + } + // target="_blank" normal navigation won't work and in order for the webview to follow // these links (specially within an iframe) requires us to capture the navigation and // `.cancel` it, then manually loading the URL. if policy == .allow && navigationAction.targetFrame == nil { - decisionHandler(.cancel) + decisionHandler(.cancel, preferences) load(url.absoluteString) } else { - decisionHandler(policy) + decisionHandler(policy, preferences) } } diff --git a/Sources/ForemWebView/URL+ForemUtilities.swift b/Sources/ForemWebView/URL+ForemUtilities.swift new file mode 100644 index 0000000..011f6a3 --- /dev/null +++ b/Sources/ForemWebView/URL+ForemUtilities.swift @@ -0,0 +1,9 @@ +import Foundation + +extension URL { + var isGoogleAuth: Bool { self.absoluteString.starts(with: "https://accounts.google.com") } + + var isFacebookAuth: Bool { + (self.absoluteString.starts(with: "https://facebook.com") || self.absoluteString.starts(with: "https://m.facebook.com")) && self.absoluteString.contains("/dialog/oauth?") + } +} From 764514ab021b92265aca8cd1e88b6f9d3a484ea7 Mon Sep 17 00:00:00 2001 From: Josh Puetz Date: Fri, 4 Feb 2022 16:18:30 -0600 Subject: [PATCH 2/3] Refactor and add tests --- Package.resolved | 9 ++ Package.swift | 10 +- Sources/ForemWebView/ForemWebView.swift | 27 +--- Sources/ForemWebView/URL+ForemUtilities.swift | 55 +++++++- .../URL+ForemUtilitiesTests.swift | 119 ++++++++++++++++++ Tests/ForemWebViewTests/XCTestManifests.swift | 1 + 6 files changed, 190 insertions(+), 31 deletions(-) create mode 100644 Tests/ForemWebViewTests/URL+ForemUtilitiesTests.swift diff --git a/Package.resolved b/Package.resolved index 61f45da..d7e93d8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -19,6 +19,15 @@ "version": "4.2.0" } }, + { + "package": "Fakery", + "repositoryURL": "https://github.com/vadymmarkov/Fakery", + "state": { + "branch": null, + "revision": "71cb3bf36a808534659d1248780c2bf3c4c4fc91", + "version": "5.1.0" + } + }, { "package": "PryntTrimmerView", "repositoryURL": "https://github.com/HHK1/PryntTrimmerView", diff --git a/Package.swift b/Package.swift index 6d912f7..42f1cb2 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,7 @@ let package = Package( .package(url: "https://github.com/Yummypets/YPImagePicker", .revision("2cf2d150bb0861f2079bc44b56c17aabf5e5d5aa")), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), + .package(url: "https://github.com/vadymmarkov/Fakery", .upToNextMajor(from: "5.0.0")), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -29,6 +30,13 @@ let package = Package( .product(name: "YPImagePicker", package: "YPImagePicker", condition: .when(platforms: [.iOS])), ], resources: []), - .testTarget(name: "ForemWebViewTests", dependencies: ["ForemWebView"], resources: [.process("Assets")]) + .testTarget( + name: "ForemWebViewTests", + dependencies: [ + "ForemWebView", + .product(name: "Fakery", package: "Fakery"), + ], + resources: [.process("Assets")] + ) ] ) diff --git a/Sources/ForemWebView/ForemWebView.swift b/Sources/ForemWebView/ForemWebView.swift index 18405e3..f9acfc1 100644 --- a/Sources/ForemWebView/ForemWebView.swift +++ b/Sources/ForemWebView/ForemWebView.swift @@ -62,32 +62,7 @@ open class ForemWebView: WKWebView { } class func isOAuthUrl(_ url: URL) -> Bool { - // GitHub OAuth paths including 2FA + error pages - if url.absoluteString.hasPrefix("https://github.com/login") || - url.absoluteString.hasPrefix("https://github.com/session") { - return true - } - - // Twitter OAuth paths including error pages - if url.absoluteString.hasPrefix("https://api.twitter.com/oauth") || - url.absoluteString.hasPrefix("https://twitter.com/login/error") { - return true - } - - // Regex for Facebook OAuth based on their API versions - // Example: "https://www.facebook.com/v4.0/dialog/oauth" - let fbRegex = #"https://(www|m)?\.facebook\.com/(v\d+.\d+/dialog/oauth|login.php)"# - if url.absoluteString.range(of: fbRegex, options: .regularExpression) != nil { - return true - } - - // Forem Passport Auth - if url.absoluteString.hasPrefix("https://passport.forem.com/oauth") { - return true - } - - // Didn't match any supported OAuth URL - return false + url.isOAuthUrl() } func setupWebView() { diff --git a/Sources/ForemWebView/URL+ForemUtilities.swift b/Sources/ForemWebView/URL+ForemUtilities.swift index 011f6a3..a0f23c1 100644 --- a/Sources/ForemWebView/URL+ForemUtilities.swift +++ b/Sources/ForemWebView/URL+ForemUtilities.swift @@ -1,9 +1,56 @@ import Foundation -extension URL { - var isGoogleAuth: Bool { self.absoluteString.starts(with: "https://accounts.google.com") } - +public extension URL { + // Regex for Facebook OAuth based on their API versions + // Example: "https://www.facebook.com/v4.0/dialog/oauth" + static let faceBookRegex = #"https://(www|m)?\.facebook\.com/(v\d+.\d+/dialog/oauth|login.php)"# var isFacebookAuth: Bool { - (self.absoluteString.starts(with: "https://facebook.com") || self.absoluteString.starts(with: "https://m.facebook.com")) && self.absoluteString.contains("/dialog/oauth?") + self.absoluteString.range(of: URL.faceBookRegex, options: .regularExpression) != nil + } + + // Forem Passport Auth + var isForemPassportAuth: Bool { self.absoluteString.hasPrefix("https://passport.forem.com/oauth") } + + // GitHub OAuth paths including 2FA + error pages + var isGithubAuth: Bool { + self.absoluteString.hasPrefix("https://github.com/login") || + self.absoluteString.hasPrefix("https://github.com/session") + } + + //Google OAuth pages + var isGoogleAuth: Bool { self.absoluteString.hasPrefix("https://accounts.google.com") } + + // Twitter OAuth paths including error pages + var isTwitterAuth: Bool { + self.absoluteString.hasPrefix("https://api.twitter.com/oauth") || + self.absoluteString.hasPrefix("https://twitter.com/login/error") + } + + + func isOAuthUrl() -> Bool { + + + if isGithubAuth { + return true + } + + if isTwitterAuth { + return true + } + + if isFacebookAuth { + return true + } + + if isGoogleAuth { + return true + } + + if isForemPassportAuth { + return true + } + + // Didn't match any supported OAuth URL + return false } } diff --git a/Tests/ForemWebViewTests/URL+ForemUtilitiesTests.swift b/Tests/ForemWebViewTests/URL+ForemUtilitiesTests.swift new file mode 100644 index 0000000..d9bf7e4 --- /dev/null +++ b/Tests/ForemWebViewTests/URL+ForemUtilitiesTests.swift @@ -0,0 +1,119 @@ +import XCTest +import Fakery + +class URL_ForemUtilitiesTests: XCTestCase { + + static var allTests = [ + ("testIsOauthURL", testIsOauthURL), + ("testIsGoogleAuth", testIsGithubAuth), + ("testIsGithubAuth", testIsGoogleAuth), + ("testIsFacebookAuth", testIsFacebookAuth), + ("testIsForemPassportAuth", testIsForemPassportAuth), + ("testIsTwitterAuth", testIsTwitterAuth), + ] + static let faker = Faker() + + static let githubUrlStrings = [ + "https://github.com/login", + "https://github.com/sessions/two-factor", + """ + https://github.com/login?client_id=123123123123& + return_to=%2Flogin%2Foauth%2Fauthorize%3Fclient_id%3Dd7251d40ac9298bdd9fe%26redirect_uri%3D + https%253A%252F%252Fdev.to%252Fusers%252Fauth%252Fgithub%252Fcallback%26response_type%3D + code%26scope%3Duser%253Aemail%26state%3Dfb251bee9df12312312313d6e228bdc63 + """, + ] + + static let twitterUrlStrings = [ + "https://api.twitter.com/oauth", + "https://api.twitter.com/oauth/authenticate?oauth_token=-_1DwgA123123123YqVY", + """ + https://twitter.com/login/error?username_or_email=asdasda&redirect_after_login= + https%3A%2F%2Fapi.twitter.com%2Foauth%2Fauthenticate%3Foauth_token%3D-_1DwgAAAAAAa8cGAAABdXEYqVY + """, + ] + + static let facebookUrlStrings = [ + "https://www.facebook.com/v4.0/dialog/oauth", + "https://www.facebook.com/v5.9/dialog/oauth", + "https://www.facebook.com/v6.0/dialog/oauth", + "https://m.facebook.com/v4.0/dialog/oauth", + "https://m.facebook.com/v6.0/dialog/oauth", + "https://m.facebook.com/login.php?skip_api_login=1&api_key=asdf", + ] + + static let passportUrlStrings = [ + "https://passport.forem.com/oauth/authorize?client_id=IBex_ltWo0tiuoB9CgHt7LCrwTuG5rlwhphjzQdf1RA&redirect_uri=https%3A%2F%2Fgggames.visualcosita.com%2Fusers%2Fauth%2Fforem%2Fcallback&response_type=code&state=de3f6b0c4cac41fdb9abf5409ce2f24e2d743245ca37a53a" + ] + + static let googleUrlStrings = [ + "https://accounts.google.com/o/oauth2/v2/auth", + """ + https://accounts.google.com/o/oauth2/v2/auth? + scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly& + access_type=offline& + include_granted_scopes=true& + response_type=code& + state=state_parameter_passthrough_value& + redirect_uri=https%3A//oauth2.example.com/code& + client_id=client_id + """, + ] + + static let urlStrings = githubUrlStrings + twitterUrlStrings + facebookUrlStrings + passportUrlStrings + googleUrlStrings + + func testIsOauthURL() { + for urlString in URL_ForemUtilitiesTests.urlStrings { + if let url = URL(string: urlString) { + XCTAssertTrue(url.isOAuthUrl(), "String didn't match as Auth URL: \(urlString)") + } + } + + for _ in 0...5 { + if let url = URL(string: URL_ForemUtilitiesTests.faker.internet.url()) { + XCTAssertFalse(url.isOAuthUrl(), "String incorrectly identified a Auth URL: \(url.absoluteString)") + } + } + } + + func testIsGithubAuth() { + for urlString in URL_ForemUtilitiesTests.githubUrlStrings { + if let url = URL(string: urlString) { + XCTAssertTrue(url.isGithubAuth, "String didn't match as Auth URL: \(urlString)") + } + } + } + + func testIsGoogleAuth() { + for urlString in URL_ForemUtilitiesTests.googleUrlStrings { + if let url = URL(string: urlString) { + XCTAssertTrue(url.isGoogleAuth, "String didn't match as Auth URL: \(urlString)") + } + } + } + + func testIsFacebookAuth() { + for urlString in URL_ForemUtilitiesTests.facebookUrlStrings { + if let url = URL(string: urlString) { + XCTAssertTrue(url.isFacebookAuth, "String didn't match as Auth URL: \(urlString)") + } + } + } + + func testIsForemPassportAuth() { + for urlString in URL_ForemUtilitiesTests.passportUrlStrings { + if let url = URL(string: urlString) { + XCTAssertTrue(url.isForemPassportAuth, "String didn't match as Auth URL: \(urlString)") + } + } + } + + func testIsTwitterAuth() { + for urlString in URL_ForemUtilitiesTests.twitterUrlStrings { + if let url = URL(string: urlString) { + XCTAssertTrue(url.isTwitterAuth, "String didn't match as Auth URL: \(urlString)") + } + } + } + +} diff --git a/Tests/ForemWebViewTests/XCTestManifests.swift b/Tests/ForemWebViewTests/XCTestManifests.swift index 466cfb4..1212403 100644 --- a/Tests/ForemWebViewTests/XCTestManifests.swift +++ b/Tests/ForemWebViewTests/XCTestManifests.swift @@ -4,6 +4,7 @@ import XCTest public func allTests() -> [XCTestCaseEntry] { return [ testCase(ForemWebViewTests.allTests), + testCase(URL_ForemUtilitiesTests.allTests), ] } #endif From bfd8577b4baf207d73313ace5ce097cd42ae497a Mon Sep 17 00:00:00 2001 From: Josh Puetz Date: Fri, 4 Feb 2022 16:23:39 -0600 Subject: [PATCH 3/3] Update docs --- docs/ForemWebView-deep-dive.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/ForemWebView-deep-dive.md b/docs/ForemWebView-deep-dive.md index bfb701a..b5ba935 100644 --- a/docs/ForemWebView-deep-dive.md +++ b/docs/ForemWebView-deep-dive.md @@ -19,9 +19,6 @@ It's important to know this is a custom implementation of `WKWebView` and you ** - `load(_ urlString: String)` - Helper method for simplicity: `webView.load("https://dev.to")` -- `isOAuthUrl(_ url: URL) -> Bool` - - Responds to whether the url provided is one of the supported 3rd party redirect URLs in a OAuth protocol - - Useful if implementing `WKNavigationDelegate` on your own (not recommended) - `userData` - Instance of `ForemUserData` when authenticated or `nil` otherwise - `foremInstance` @@ -31,6 +28,12 @@ It's important to know this is a custom implementation of `WKWebView` and you ** - Instead of polling with this function we recommend you register to observe the `userData` variable as you'll react to changes when they become available - `fetchUserData(completion: @escaping (ForemUserData?) -> Void)` +Extension to `URL` + +- `.isOAuthUrl -> Bool` + - Responds to whether the url is one of the supported 3rd party redirect URLs in a OAuth protocol + - Useful if implementing `WKNavigationDelegate` on your own (not recommended) + ## Native Podcast Player & Picture in Picture video In order for your App to take advantage of these native features via the `ForemWebView` you'll need to configure a few things: