<commit_before>
/**
 *  This class provides a simple interface for performing authentication tasks.
 */
class WMFAuthenticationManager: NSObject {    
    private var keychainCredentials:WMFKeychainCredentials
    
    /**
     *  The current logged in user. If nil, no user is logged in
     */
    @objc dynamic private(set) var loggedInUsername: String? = nil
    
    /**
     *  Returns YES if a user is logged in, NO otherwise
     */
    @objc public var isLoggedIn: Bool {
        return (loggedInUsername != nil)
    }

    @objc public var hasKeychainCredentials: Bool {
        guard
            let userName = keychainCredentials.userName,
            userName.count > 0,
            let password = keychainCredentials.password,
            password.count > 0
            else {
                return false
        }
        return true
    }

    private let loginInfoFetcher = WMFAuthLoginInfoFetcher()
    private let tokenFetcher = WMFAuthTokenFetcher()
    private let accountLogin = WMFAccountLogin()
    private let currentlyLoggedInUserFetcher = WMFCurrentlyLoggedInUserFetcher()
    
    /**
     *  Get the shared instance of this class
     *
     *  @return The shared Authentication Manager
     */
    @objc public static let sharedInstance = WMFAuthenticationManager()    

    override private init() {
        keychainCredentials = WMFKeychainCredentials()
    }
    
    var loginSiteURL: URL {
        var baseURL: URL?
        if let host = self.keychainCredentials.host {
            var components = URLComponents()
            components.host = host
            components.scheme = "https"
            baseURL = components.url
        }
        
        if baseURL == nil {
//            #if DEBUG
//                let loginHost = "readinglists.wmflabs.org"
//                let loginScheme = "https"
//                var components = URLComponents()
//                components.host = loginHost
//                components.scheme = loginScheme
//                baseURL = components.url
//            #else
                baseURL = MWKLanguageLinkController.sharedInstance().appLanguage?.siteURL()
//            #endif
        }
        
        return baseURL!
    }
    
    /**
     *  Login with the given username and password
     *
     *  @param username The username to authenticate
     *  @param password The <PASSWORD> the user
     *  @param retypePassword The password used for confirming password changes. Optional.
     *  @param oathToken Two factor password required if user's account has 2FA enabled. Optional.
     *  @param success  The handler for success - at this point the user is logged in
     *  @param failure     The handler for any errors
     */
    @objc public func login(username: String, password:String, retypePassword:String?, oathToken:String?, captchaID: String?, captchaWord: String?, success loginSuccess:@escaping WMFAccountLoginResultBlock, failure:@escaping WMFErrorHandler){
        let siteURL = loginSiteURL
        self.tokenFetcher.fetchToken(ofType: .login, siteURL: siteURL, success: { tokenBlock in
            self.accountLogin.login(username: username, password: password, retypePassword: retypePassword, loginToken: tokenBlock.token, oathToken: oathToken, captchaID: captchaID, captchaWord: captchaWord, siteURL: siteURL, success: {result in
                let normalizedUserName = result.username
                self.loggedInUsername = normalizedUserName
                self.keychainCredentials.userName = normalizedUserName
                self.keychainCredentials.password = password
                self.keychainCredentials.host = siteURL.host
                self.cloneSessionCookies()
                SessionSingleton.sharedInstance()?.dataStore.clearMemoryCache()
                loginSuccess(result)
            }, failure: failure)
        }, failure:failure)
    }
    
    /**
     *  Logs in a user using saved credentials in the keychain
     *
     *  @param success  The handler for success - at this point the user is logged in
     *  @param userWasAlreadyLoggedIn     The handler called if a user was found to already be logged in
     *  @param failure     The handler for any errors
     */
    @objc public func loginWithSavedCredentials(success:@escaping WMFAccountLoginResultBlock, userAlreadyLoggedInHandler:@escaping WMFCurrentlyLoggedInUserBlock, failure:@escaping WMFErrorHandler){
        
        guard hasKeychainCredentials,
            let userName = keychainCredentials.userName,
            let password = keychainCredentials.password
        else {
            failure(WMFCurrentlyLoggedInUserFetcherError.blankUsernameOrPassword)
            return
        }
        
        let siteURL = loginSiteURL
        currentlyLoggedInUserFetcher.fetch(siteURL: siteURL, success: { result in
            self.loggedInUsername = result.name
            userAlreadyLoggedInHandler(result)
        }, failure:{ error in
            self.loggedInUsername = nil
            
            self.login(username: userName, password: password, retypePassword: <PASSWORD>, oathToken: nil, captchaID: nil, captchaWord: nil, success: success, failure: { error in
                if let error = error as? URLError {
                    if error.code != .notConnectedToInternet {
                        self.logout(success:WMFIgnoreSuccessHandler, failure:WMFIgnoreErrorHandler)
                    }
                }
                failure(error)
            })
        })
    }
    
    private var logoutManager:AFHTTPSessionManager?
    
    /**
     *  Logs out any authenticated user and clears out any associated cookies
     */
    @objc public func logout(success:@escaping WMFSuccessHandler, failure:@escaping WMFErrorHandler){
        let outerSuccess = success
        let outerFailure = failure
        logoutManager = AFHTTPSessionManager(baseURL: loginSiteURL)
        _ = logoutManager?.wmf_apiPOSTWithParameters(["action": "logout", "format": "json"], success: { (_, response) in
            
            self.keychainCredentials.userName = nil
            self.keychainCredentials.password = <PASSWORD>
            self.loggedInUsername = nil
            // Cookie reminders: 
            //  - Call "action=logout" API *before* clearing app cookies.
            //  - "HTTPCookieStorage.shared.removeCookies(since: Date.distantPast)" does NOT seem to work.
            HTTPCookieStorage.shared.cookies?.forEach { cookie in
                HTTPCookieStorage.shared.deleteCookie(cookie)
            }
            SessionSingleton.sharedInstance()?.dataStore.clearMemoryCache()
            
            outerSuccess()
        }, failure: { (_, error) in
            outerFailure(error)
        })
    }
    
    func cloneSessionCookies() {
        // Make the session cookies expire at same time user cookies. Just remember they still can't be
        // necessarily assumed to be valid as the server may expire them, but at least make them last as<commit_msg>Method for clearing keychain credentials.<commit_after>
/**
 *  This class provides a simple interface for performing authentication tasks.
 */
class WMFAuthenticationManager: NSObject {    
    private var keychainCredentials:WMFKeychainCredentials
    
    /**
     *  The current logged in user. If nil, no user is logged in
     */
    @objc dynamic private(set) var loggedInUsername: String? = nil
    
    /**
     *  Returns YES if a user is logged in, NO otherwise
     */
    @objc public var isLoggedIn: Bool {
        return (loggedInUsername != nil)
    }

    @objc public var hasKeychainCredentials: Bool {
        guard
            let userName = keychainCredentials.userName,
            userName.count > 0,
            let password = keychainCredentials.password,
            password.count > 0
            else {
                return false
        }
        return true
    }

    @objc public func clearKeychainCredentials() {
        self.keychainCredentials.userName = nil
        self.keychainCredentials.password = <PASSWORD>
    }
    
    private let loginInfoFetcher = WMFAuthLoginInfoFetcher()
    private let tokenFetcher = WMFAuthTokenFetcher()
    private let accountLogin = WMFAccountLogin()
    private let currentlyLoggedInUserFetcher = WMFCurrentlyLoggedInUserFetcher()
    
    /**
     *  Get the shared instance of this class
     *
     *  @return The shared Authentication Manager
     */
    @objc public static let sharedInstance = WMFAuthenticationManager()    

    override private init() {
        keychainCredentials = WMFKeychainCredentials()
    }
    
    var loginSiteURL: URL {
        var baseURL: URL?
        if let host = self.keychainCredentials.host {
            var components = URLComponents()
            components.host = host
            components.scheme = "https"
            baseURL = components.url
        }
        
        if baseURL == nil {
//            #if DEBUG
//                let loginHost = "readinglists.wmflabs.org"
//                let loginScheme = "https"
//                var components = URLComponents()
//                components.host = loginHost
//                components.scheme = loginScheme
//                baseURL = components.url
//            #else
                baseURL = MWKLanguageLinkController.sharedInstance().appLanguage?.siteURL()
//            #endif
        }
        
        return baseURL!
    }
    
    /**
     *  Login with the given username and password
     *
     *  @param username The username to authenticate
     *  @param password The <PASSWORD>
     *  @param retypePassword The password used for confirming password changes. Optional.
     *  @param oathToken Two factor password required if user's account has 2FA enabled. Optional.
     *  @param success  The handler for success - at this point the user is logged in
     *  @param failure     The handler for any errors
     */
    @objc public func login(username: String, password:String, retypePassword:String?, oathToken:String?, captchaID: String?, captchaWord: String?, success loginSuccess:@escaping WMFAccountLoginResultBlock, failure:@escaping WMFErrorHandler){
        let siteURL = loginSiteURL
        self.tokenFetcher.fetchToken(ofType: .login, siteURL: siteURL, success: { tokenBlock in
            self.accountLogin.login(username: username, password: password, retypePassword: <PASSWORD>, loginToken: tokenBlock.token, oathToken: oathToken, captchaID: captchaID, captchaWord: captchaWord, siteURL: siteURL, success: {result in
                let normalizedUserName = result.username
                self.loggedInUsername = normalizedUserName
                self.keychainCredentials.userName = normalizedUserName
                self.keychainCredentials.password = password
                self.keychainCredentials.host = siteURL.host
                self.cloneSessionCookies()
                SessionSingleton.sharedInstance()?.dataStore.clearMemoryCache()
                loginSuccess(result)
            }, failure: failure)
        }, failure:failure)
    }
    
    /**
     *  Logs in a user using saved credentials in the keychain
     *
     *  @param success  The handler for success - at this point the user is logged in
     *  @param userWasAlreadyLoggedIn     The handler called if a user was found to already be logged in
     *  @param failure     The handler for any errors
     */
    @objc public func loginWithSavedCredentials(success:@escaping WMFAccountLoginResultBlock, userAlreadyLoggedInHandler:@escaping WMFCurrentlyLoggedInUserBlock, failure:@escaping WMFErrorHandler){
        
        guard hasKeychainCredentials,
            let userName = keychainCredentials.userName,
            let password = keychainCredentials.password
        else {
            failure(WMFCurrentlyLoggedInUserFetcherError.blankUsernameOrPassword)
            return
        }
        
        let siteURL = loginSiteURL
        currentlyLoggedInUserFetcher.fetch(siteURL: siteURL, success: { result in
            self.loggedInUsername = result.name
            userAlreadyLoggedInHandler(result)
        }, failure:{ error in
            self.loggedInUsername = nil
            
            self.login(username: userName, password: password, retypePassword: nil, oathToken: nil, captchaID: nil, captchaWord: nil, success: success, failure: { error in
                if let error = error as? URLError {
                    if error.code != .notConnectedToInternet {
                        self.logout(success:WMFIgnoreSuccessHandler, failure:WMFIgnoreErrorHandler)
                    }
                }
                failure(error)
            })
        })
    }
    
    private var logoutManager:AFHTTPSessionManager?
    
    /**
     *  Logs out any authenticated user and clears out any associated cookies
     */
    @objc public func logout(success:@escaping WMFSuccessHandler, failure:@escaping WMFErrorHandler){
        let outerSuccess = success
        let outerFailure = failure
        logoutManager = AFHTTPSessionManager(baseURL: loginSiteURL)
        _ = logoutManager?.wmf_apiPOSTWithParameters(["action": "logout", "format": "json"], success: { (_, response) in
            
            self.clearKeychainCredentials()
            self.loggedInUsername = nil
            // Cookie reminders: 
            //  - Call "action=logout" API *before* clearing app cookies.
            //  - "HTTPCookieStorage.shared.removeCookies(since: Date.distantPast)" does NOT seem to work.
            HTTPCookieStorage.shared.cookies?.forEach { cookie in
                HTTPCookieStorage.shared.deleteCookie(cookie)
            }
            SessionSingleton.sharedInstance()?.dataStore.clearMemoryCache()
            
            outerSuccess()
        }, failure: { (_, error) in
            outerFailure(error)
        })
    }
    
    func cloneSessionCookies() {
        // Make the session cookies expire at same time user cookies. Just remember they still can't be
        // necessarily assumed to be valid as the server may expire them, but at least make them last as
{'fraction_non_alphanumeric': 0.0722900113507378, 'fraction_numerical': 0.0004256526674233825, 'mean_word_length': 2.2104304258711, 'pattern_counts': {'":': 4, '<': 9, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2392524', 'n_tokens_mistral': 3633, 'n_tokens_neox': 3562, 'n_words': 1008}