11// SPDX-FileCopyrightText: Nextcloud GmbH
2+ // SPDX-FileCopyrightText: 2025 Iva Horn
23// SPDX-FileCopyrightText: 2025 Milen Pivchev
34// SPDX-License-Identifier: GPL-3.0-or-later
45
@@ -7,91 +8,113 @@ import UIKit
78import NextcloudKit
89import FloatingPanel
910
11+ protocol NCLoginProviderDelegate : AnyObject {
12+ ///
13+ /// Called when the back button is tapped in the login provider view.
14+ ///
15+ func onBack( )
16+ }
17+
18+ ///
19+ /// View which presents the web view to login at a Nextcloud instance.
20+ ///
1021class NCLoginProvider : UIViewController {
11- var webView : WKWebView ?
12- let utility = NCUtility ( )
22+ var logger = NextcloudKit . shared . nkCommonInstance
23+ var webView : WKWebView !
1324 var titleView : String = " "
14- var urlBase = " "
25+ var initialURLString = " "
1526 var uiColor : UIColor = . white
1627 weak var delegate : NCLoginProviderDelegate ?
1728 var controller : NCMainTabBarController ?
1829
30+ ///
31+ /// A polling loop active in the background to check for the current status of the login flow.
32+ ///
1933 var pollingTask : Task < Void , any Error > ?
2034
2135 // MARK: - View Life Cycle
2236
2337 override func viewDidLoad( ) {
2438 super. viewDidLoad ( )
39+ logger. writeLog ( info: " Login provider view did load. " )
40+ let configuration = WKWebViewConfiguration ( )
41+ configuration. websiteDataStore = WKWebsiteDataStore . nonPersistent ( )
2542
26- webView = WKWebView ( frame: CGRect . zero, configuration: WKWebViewConfiguration ( ) )
27- if let webView {
28- webView. navigationDelegate = self
29- view. addSubview ( webView)
43+ let webView = WKWebView ( frame: CGRect . zero, configuration: configuration)
44+ webView. customUserAgent = userAgent
3045
31- webView. translatesAutoresizingMaskIntoConstraints = false
32- webView. leadingAnchor. constraint ( equalTo: view. leadingAnchor, constant: 0 ) . isActive = true
33- webView. rightAnchor. constraint ( equalTo: view. rightAnchor, constant: 0 ) . isActive = true
34- webView. topAnchor. constraint ( equalTo: view. topAnchor, constant: 0 ) . isActive = true
35- webView. bottomAnchor. constraint ( equalTo: view. bottomAnchor, constant: 0 ) . isActive = true
46+ if #available( iOS 16 . 4 , * ) {
47+ webView. isInspectable = true
3648 }
3749
50+ webView. navigationDelegate = self
51+ view. addSubview ( webView)
52+
53+ webView. translatesAutoresizingMaskIntoConstraints = false
54+ webView. leadingAnchor. constraint ( equalTo: view. leadingAnchor, constant: 0 ) . isActive = true
55+ webView. rightAnchor. constraint ( equalTo: view. rightAnchor, constant: 0 ) . isActive = true
56+ webView. topAnchor. constraint ( equalTo: view. topAnchor, constant: 0 ) . isActive = true
57+ webView. bottomAnchor. constraint ( equalTo: view. bottomAnchor, constant: 0 ) . isActive = true
58+
59+ self . webView = webView
60+
3861 let navigationItemBack = UIBarButtonItem ( image: UIImage ( systemName: " arrow.left " ) , style: . done, target: self , action: #selector( goBack ( _: ) ) )
3962 navigationItemBack. tintColor = uiColor
4063 navigationItem. leftBarButtonItem = navigationItemBack
4164 }
4265
4366 override func viewDidAppear( _ animated: Bool ) {
4467 super. viewDidAppear ( animated)
68+ logger. writeLog ( info: " Login provider appeared. " )
4569
46- if let url = URL ( string: urlBase) ,
47- let webView {
48- HTTPCookieStorage . shared. removeCookies ( since: Date . distantPast)
49-
50- WKWebsiteDataStore . default ( ) . fetchDataRecords ( ofTypes: WKWebsiteDataStore . allWebsiteDataTypes ( ) ) { records in
51- WKWebsiteDataStore . default ( ) . removeData ( ofTypes: WKWebsiteDataStore . allWebsiteDataTypes ( ) , for: records, completionHandler: {
52- self . loadWebPage ( webView: webView, url: url)
53- } )
54- }
55- } else {
70+ guard let url = URL ( string: initialURLString) else {
5671 let error = NKError ( errorCode: NCGlobal . shared. errorInternalError, errorDescription: " _login_url_error_ " )
5772 NCContentPresenter ( ) . showError ( error: error, priority: . max)
73+ return
5874 }
5975
60- if let host = URL ( string : urlBase ) ? . host {
76+ if let host = url . host {
6177 titleView = host
78+
6279 if let activeTableAccount = NCManageDatabase . shared. getActiveTableAccount ( ) , NCKeychain ( ) . getPassword ( account: activeTableAccount. account) . isEmpty {
6380 titleView = NSLocalizedString ( " _user_ " , comment: " " ) + " " + activeTableAccount. userId + " " + NSLocalizedString( " _in_ " , comment: " " ) + " " + host
6481 }
6582 }
6683
84+ loadWebPage ( url: url)
6785 self . title = titleView
6886 }
6987
7088 override func viewDidDisappear( _ animated: Bool ) {
7189 super. viewDidDisappear ( animated)
90+ logger. writeLog ( info: " Login provider view did disappear. " )
91+
7292 NCActivityIndicator . shared. stop ( )
7393
94+ guard pollingTask != nil else {
95+ return
96+ }
97+
98+ logger. writeLog ( info: " Cancelling existing polling task because view did disappear... " )
7499 pollingTask? . cancel ( )
75100 pollingTask = nil
76101 }
77102
78- func loadWebPage( webView: WKWebView , url: URL ) {
103+ // MARK: - Navigation
104+
105+ private func loadWebPage( url: URL ) {
79106 let language = NSLocale . preferredLanguages [ 0 ] as String
80107 var request = URLRequest ( url: url)
81108
82- if let deviceName = " \( UIDevice . current. name) ( \( NCBrandOptions . shared. brand) iOS) " . cString ( using: . utf8) ,
83- let deviceUserAgent = String ( cString: deviceName, encoding: . ascii) {
84- webView. customUserAgent = deviceUserAgent
85- } else {
86- webView. customUserAgent = userAgent
87- }
88-
89109 request. addValue ( " true " , forHTTPHeaderField: " OCS-APIRequest " )
90110 request. addValue ( language, forHTTPHeaderField: " Accept-Language " )
91111
92112 webView. load ( request)
93113 }
94114
115+ ///
116+ /// Dismiss the login web view from the hierarchy.
117+ ///
95118 @objc func goBack( _ sender: Any ? ) {
96119 delegate? . onBack ( )
97120
@@ -102,41 +125,88 @@ class NCLoginProvider: UIViewController {
102125 }
103126 }
104127
128+ // MARK: - Polling
129+
130+ ///
131+ /// Start checking the status of the login flow in the background periodally.
132+ ///
105133 func startPolling( loginFlowV2Token: String , loginFlowV2Endpoint: String , loginFlowV2Login: String ) {
106- pollingTask = poll ( loginFlowV2Token: loginFlowV2Token, loginFlowV2Endpoint: loginFlowV2Endpoint, loginFlowV2Login: loginFlowV2Login)
134+ logger. writeLog ( info: " Starting polling at \( loginFlowV2Endpoint) with token \( loginFlowV2Token) " )
135+ pollingTask = createPollingTask ( token: loginFlowV2Token, endpoint: loginFlowV2Endpoint)
136+ logger. writeLog ( info: " Polling task created. " )
107137 }
108138
109- private func getPollResponse( loginFlowV2Token: String , loginFlowV2Endpoint: String , loginOptions: NKRequestOptions ) async -> ( urlBase: String , loginName: String , appPassword: String ) ? {
139+ ///
140+ /// Fetch the server response and process it.
141+ ///
142+ private func poll( token: String , endpoint: String , options: NKRequestOptions ) async -> ( urlBase: String , loginName: String , appPassword: String ) ? {
110143 await withCheckedContinuation { continuation in
111- NextcloudKit . shared. getLoginFlowV2Poll ( token: loginFlowV2Token, endpoint: loginFlowV2Endpoint, options: loginOptions) { server, loginName, appPassword, _, error in
112- if error == . success, let urlBase = server, let user = loginName, let appPassword {
113- continuation. resume ( returning: ( urlBase, user, appPassword) )
114- } else {
144+ NextcloudKit . shared. getLoginFlowV2Poll ( token: token, endpoint: endpoint, options: options) { [ weak self] server, loginName, appPassword, _, error in
145+ guard let self else {
146+ return
147+ }
148+
149+ guard error == . success else {
150+ logger. writeLog ( error: " Login poll result for token \" \( token) \" is not successful! " )
151+ continuation. resume ( returning: nil )
152+ return
153+ }
154+
155+ guard let urlBase = server else {
156+ logger. writeLog ( error: " Login poll response field for server for token \" \( token) \" is nil! " )
157+ continuation. resume ( returning: nil )
158+ return
159+ }
160+
161+ guard let user = loginName else {
162+ logger. writeLog ( error: " Login poll response field for user name for token \" \( token) \" is nil! " )
163+ continuation. resume ( returning: nil )
164+ return
165+ }
166+
167+ guard let appPassword = appPassword else {
168+ logger. writeLog ( error: " Login poll response field for app password for token \" \( token) \" is nil! " )
115169 continuation. resume ( returning: nil )
170+ return
116171 }
172+
173+ logger. writeLog ( info: " Returning login poll response for \" \( user) \" on \" \( urlBase) \" for token \" \( token) \" . " )
174+ continuation. resume ( returning: ( urlBase, user, appPassword) )
117175 }
118176 }
119177 }
120178
179+ ///
180+ /// Handle the values acquired by polling successfully.
181+ ///
121182 private func handleGrant( urlBase: String , loginName: String , appPassword: String ) async {
183+ logger. writeLog ( info: " Handling login grant values for \( loginName) on \( urlBase) " )
184+
122185 await withCheckedContinuation { continuation in
123186 if controller == nil {
187+ logger. writeLog ( info: " View controller is still undefined, will resolve root view controller of first window. " )
124188 controller = UIApplication . shared. firstWindow? . rootViewController as? NCMainTabBarController
125189 }
126190
127191 NCAccount ( ) . createAccount ( viewController: self , urlBase: urlBase, user: loginName, password: appPassword, controller: controller) {
128- continuation. resume ( )
192+ self . logger. writeLog ( info: " Account creation for \( loginName) on \( urlBase) completed based on login grant values. " )
193+ continuation. resume ( )
129194 }
130195 }
131196 }
132197
133- private func poll( loginFlowV2Token: String , loginFlowV2Endpoint: String , loginFlowV2Login: String ) -> Task < Void , any Error > {
134- let loginOptions = NKRequestOptions ( customUserAgent: userAgent)
198+ ///
199+ /// Set up the `Task` which frequently checks the server.
200+ ///
201+ private func createPollingTask( token: String , endpoint: String ) -> Task < Void , any Error > {
202+ let options = NKRequestOptions ( customUserAgent: userAgent)
135203 var grantValues : ( urlBase: String , loginName: String , appPassword: String ) ?
136204
137205 return Task { @MainActor in
138206 repeat {
139- grantValues = await getPollResponse ( loginFlowV2Token: loginFlowV2Token, loginFlowV2Endpoint: loginFlowV2Endpoint, loginOptions: loginOptions)
207+ try Task . checkCancellation ( )
208+
209+ grantValues = await poll ( token: token, endpoint: endpoint, options: options)
140210 try await Task . sleep ( nanoseconds: 1_000_000_000 ) // .seconds() is not supported on iOS 15 yet.
141211 } while grantValues == nil
142212
@@ -145,32 +215,48 @@ class NCLoginProvider: UIViewController {
145215 }
146216
147217 await handleGrant ( urlBase: grantValues. urlBase, loginName: grantValues. loginName, appPassword: grantValues. appPassword)
218+ logger. writeLog ( info: " Polling task completed. " )
148219 }
149220 }
150221}
151222
223+ // MARK: - WKNavigationDelegate
224+
152225extension NCLoginProvider : WKNavigationDelegate {
153226 func webView( _ webView: WKWebView , didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation ! ) {
154- guard let url = webView. url else { return }
155- let urlString : String = url. absoluteString. lowercased ( )
227+ logger. writeLog ( info: " Web view did receive server redirect for provisional navigation. " )
228+
229+ guard let currentWebViewURL = webView. url else {
230+ logger. writeLog ( error: " Web view does not have a URL after receiving a server redirect for provisional navigation! " )
231+ return
232+ }
233+
234+ let currentWebViewURLString : String = currentWebViewURL. absoluteString. lowercased ( )
235+
236+ // Prevent HTTP redirects.
237+ if initialURLString. lowercased ( ) . hasPrefix ( " https:// " ) && currentWebViewURLString. hasPrefix ( " http:// " ) {
238+ logger. writeLog ( error: " Web view redirect degrades session from HTTPS to HTTP and must be cancelled! " )
156239
157- // prevent http redirection
158- if urlBase. lowercased ( ) . hasPrefix ( " https:// " ) && urlString. lowercased ( ) . hasPrefix ( " http:// " ) {
159240 let alertController = UIAlertController ( title: NSLocalizedString ( " _error_ " , comment: " " ) , message: NSLocalizedString ( " _prevent_http_redirection_ " , comment: " " ) , preferredStyle: . alert)
241+
160242 alertController. addAction ( UIAlertAction ( title: NSLocalizedString ( " _ok_ " , comment: " " ) , style: . default, handler: { _ in
161243 _ = self . navigationController? . popViewController ( animated: true )
162244 } ) )
245+
163246 self . present ( alertController, animated: true )
247+
164248 return
165249 }
166250
167- // Login via provider
168- if urlString. hasPrefix ( NCBrandOptions . shared. webLoginAutenticationProtocol) == true && urlString. contains ( " login " ) == true {
251+ // Login via provider.
252+ if currentWebViewURLString. hasPrefix ( NCBrandOptions . shared. webLoginAutenticationProtocol) && currentWebViewURLString. contains ( " login " ) {
253+ logger. writeLog ( info: " Web view redirect to provider login URL detected. " )
254+
169255 var server : String = " "
170256 var user : String = " "
171257 var password : String = " "
258+ let keyValue = currentWebViewURL. path. components ( separatedBy: " & " )
172259
173- let keyValue = url. path. components ( separatedBy: " & " )
174260 for value in keyValue {
175261 if value. contains ( " server: " ) { server = value }
176262 if value. contains ( " user: " ) { user = value }
@@ -192,6 +278,8 @@ extension NCLoginProvider: WKNavigationDelegate {
192278 }
193279
194280 func webView( _ webView: WKWebView , didReceive challenge: URLAuthenticationChallenge , completionHandler: @escaping ( URLSession . AuthChallengeDisposition , URLCredential ? ) -> Void ) {
281+ logger. writeLog ( info: " Web view did receive authentication challenge. " )
282+
195283 DispatchQueue . global ( ) . async {
196284 if let serverTrust = challenge. protectionSpace. serverTrust {
197285 completionHandler ( Foundation . URLSession. AuthChallengeDisposition. useCredential, URLCredential ( trust: serverTrust) )
@@ -202,18 +290,17 @@ extension NCLoginProvider: WKNavigationDelegate {
202290 }
203291
204292 func webView( _ webView: WKWebView , decidePolicyFor navigationAction: WKNavigationAction , decisionHandler: @escaping ( WKNavigationActionPolicy ) -> Void ) {
293+ logger. writeLog ( info: " Web view will allow navigation to \( navigationAction. request. url? . absoluteString ?? " nil " ) " )
205294 decisionHandler ( . allow)
206295 }
207296
208297 func webView( _ webView: WKWebView , didStartProvisionalNavigation navigation: WKNavigation ! ) {
298+ logger. writeLog ( info: " Web view did start provisional navigation. " )
209299 NCActivityIndicator . shared. startActivity ( backgroundView: self . view, style: . medium, blurEffect: false )
210300 }
211301
212302 func webView( _ webView: WKWebView , didFinish navigation: WKNavigation ! ) {
303+ logger. writeLog ( info: " Web view did finish navigation to \( webView. url? . absoluteString ?? " nil " ) " )
213304 NCActivityIndicator . shared. stop ( )
214305 }
215306}
216-
217- protocol NCLoginProviderDelegate : AnyObject {
218- func onBack( )
219- }
0 commit comments