element-ios/Riot/Modules/MatrixKit/Controllers/MXKAuthenticationViewContro...

2162 lines
79 KiB
Objective-C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKAuthenticationViewController.h"
#import "MXKAuthInputsEmailCodeBasedView.h"
#import "MXKAuthInputsPasswordBasedView.h"
#import "MXKAccountManager.h"
#import "NSBundle+MatrixKit.h"
#import <AFNetworking/AFNetworking.h>
#import "MXKAppSettings.h"
#import "MXKSwiftHeader.h"
#import "GeneratedInterface-Swift.h"
@interface MXKAuthenticationViewController ()
{
/**
The matrix REST client used to make matrix API requests.
*/
MXRestClient *mxRestClient;
/**
Current request in progress.
*/
MXHTTPOperation *mxCurrentOperation;
/**
The MXKAuthInputsView class or a sub-class used when logging in.
*/
Class loginAuthInputsViewClass;
/**
The MXKAuthInputsView class or a sub-class used when registering.
*/
Class registerAuthInputsViewClass;
/**
The MXKAuthInputsView class or a sub-class used to handle forgot password case.
*/
Class forgotPasswordAuthInputsViewClass;
/**
Customized block used to handle unrecognized certificate (nil by default).
*/
MXHTTPClientOnUnrecognizedCertificate onUnrecognizedCertificateCustomBlock;
/**
The current authentication fallback URL (if any).
*/
NSString *authenticationFallback;
/**
The cancel button added in navigation bar when fallback page is opened.
*/
UIBarButtonItem *cancelFallbackBarButton;
/**
The timer used to postpone the registration when the authentication is pending (for example waiting for email validation)
*/
NSTimer* registrationTimer;
/**
Identity server discovery.
*/
MXAutoDiscovery *autoDiscovery;
MXHTTPOperation *checkIdentityServerOperation;
}
/**
The identity service used to make identity server API requests.
*/
@property (nonatomic) MXIdentityService *identityService;
@property (nonatomic) AnalyticsScreenTracker *screenTracker;
@property (nonatomic) BOOL isViewVisible;
@end
@implementation MXKAuthenticationViewController
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKAuthenticationViewController class])
bundle:[NSBundle bundleForClass:[MXKAuthenticationViewController class]]];
}
+ (instancetype)authenticationViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKAuthenticationViewController class])
bundle:[NSBundle bundleForClass:[MXKAuthenticationViewController class]]];
}
#pragma mark -
- (void)finalizeInit
{
[super finalizeInit];
// Set initial auth type
_authType = MXKAuthenticationTypeLogin;
_deviceDisplayName = nil;
// Initialize authInputs view classes
loginAuthInputsViewClass = MXKAuthInputsPasswordBasedView.class;
registerAuthInputsViewClass = nil; // No registration flow is supported yet
forgotPasswordAuthInputsViewClass = nil;
}
#pragma mark -
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// Check whether the view controller has been pushed via storyboard
if (!_authenticationScrollView)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
self.authFallbackWebView = [[MXKAuthenticationFallbackWebView alloc] initWithFrame:self.authFallbackWebViewContainer.bounds];
[self.authFallbackWebViewContainer addSubview:self.authFallbackWebView];
[self.authFallbackWebView.leadingAnchor constraintEqualToAnchor:self.authFallbackWebViewContainer.leadingAnchor constant:0].active = YES;
[self.authFallbackWebView.trailingAnchor constraintEqualToAnchor:self.authFallbackWebViewContainer.trailingAnchor constant:0].active = YES;
[self.authFallbackWebView.topAnchor constraintEqualToAnchor:self.authFallbackWebViewContainer.topAnchor constant:0].active = YES;
[self.authFallbackWebView.bottomAnchor constraintEqualToAnchor:self.authFallbackWebViewContainer.bottomAnchor constant:0].active = YES;
// Load welcome image from MatrixKit asset bundle
self.welcomeImageView.image = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"logoHighRes"];
_authenticationScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
_subTitleLabel.numberOfLines = 0;
_submitButton.enabled = NO;
_authSwitchButton.enabled = YES;
_homeServerTextField.text = _defaultHomeServerUrl;
_identityServerTextField.text = _defaultIdentityServerUrl;
// Hide the identity server by default
[self setIdentityServerHidden:YES];
// Create here REST client (if homeserver is defined)
[self updateRESTClient];
// Localize labels
_homeServerLabel.text = [VectorL10n loginHomeServerTitle];
_homeServerTextField.placeholder = [VectorL10n loginServerUrlPlaceholder];
_homeServerInfoLabel.text = [VectorL10n loginHomeServerInfo];
_identityServerLabel.text = [VectorL10n loginIdentityServerTitle];
_identityServerTextField.placeholder = [VectorL10n loginServerUrlPlaceholder];
_identityServerInfoLabel.text = [VectorL10n loginIdentityServerInfo];
[_cancelAuthFallbackButton setTitle:[VectorL10n cancel] forState:UIControlStateNormal];
[_cancelAuthFallbackButton setTitle:[VectorL10n cancel] forState:UIControlStateHighlighted];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTextFieldChange:) name:UITextFieldTextDidChangeNotification object:nil];
self.isViewVisible = YES;
[self.screenTracker trackScreen];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self dismissKeyboard];
// close any opened alert
if (alert)
{
[alert dismissViewControllerAnimated:NO completion:nil];
alert = nil;
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:AFNetworkingReachabilityDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
self.isViewVisible = NO;
}
#pragma mark - Override MXKViewController
- (void)onKeyboardShowAnimationComplete
{
// Report the keyboard view in order to track keyboard frame changes
// TODO define inputAccessoryView for each text input
// and report the inputAccessoryView.superview of the firstResponder in self.keyboardView.
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
- (void)setKeyboardHeight:(CGFloat)keyboardHeight
{
// Deduce the bottom inset for the scroll view (Don't forget the potential tabBar)
CGFloat scrollViewInsetBottom = keyboardHeight - self.bottomLayoutGuide.length;
// Check whether the keyboard is over the tabBar
if (scrollViewInsetBottom < 0)
{
scrollViewInsetBottom = 0;
}
UIEdgeInsets insets = self.authenticationScrollView.contentInset;
insets.bottom = scrollViewInsetBottom;
self.authenticationScrollView.contentInset = insets;
}
#pragma clang diagnostic pop
- (void)destroy
{
self.authInputsView = nil;
if (registrationTimer)
{
[registrationTimer invalidate];
registrationTimer = nil;
}
if (mxCurrentOperation)
{
[mxCurrentOperation cancel];
mxCurrentOperation = nil;
}
[self cancelIdentityServerCheck];
[mxRestClient close];
mxRestClient = nil;
authenticationFallback = nil;
cancelFallbackBarButton = nil;
[super destroy];
}
#pragma mark - Class methods
- (void)registerAuthInputsViewClass:(Class)authInputsViewClass forAuthType:(MXKAuthenticationType)authType
{
// Sanity check: accept only MXKAuthInputsView classes or sub-classes
NSParameterAssert([authInputsViewClass isSubclassOfClass:MXKAuthInputsView.class]);
if (authType == MXKAuthenticationTypeLogin)
{
loginAuthInputsViewClass = authInputsViewClass;
}
else if (authType == MXKAuthenticationTypeRegister)
{
registerAuthInputsViewClass = authInputsViewClass;
}
else if (authType == MXKAuthenticationTypeForgotPassword)
{
forgotPasswordAuthInputsViewClass = authInputsViewClass;
}
}
- (void)setAuthType:(MXKAuthenticationType)authType
{
if (_authType != authType)
{
_authType = authType;
// Cancel external registration parameters if any
_externalRegistrationParameters = nil;
// Remove the current inputs view
self.authInputsView = nil;
isPasswordReseted = NO;
[self.authInputsContainerView bringSubviewToFront: _authenticationActivityIndicator];
[_authenticationActivityIndicator startAnimating];
}
// Restore user interaction
self.userInteractionEnabled = YES;
if (authType == MXKAuthenticationTypeLogin)
{
_subTitleLabel.hidden = YES;
[_submitButton setTitle:[VectorL10n login] forState:UIControlStateNormal];
[_submitButton setTitle:[VectorL10n login] forState:UIControlStateHighlighted];
[_authSwitchButton setTitle:[VectorL10n createAccount] forState:UIControlStateNormal];
[_authSwitchButton setTitle:[VectorL10n createAccount] forState:UIControlStateHighlighted];
// Update supported authentication flow and associated information (defined in authentication session)
[self refreshAuthenticationSession];
self.screenTracker = [[AnalyticsScreenTracker alloc] initWithScreen:AnalyticsScreenLogin];
}
else if (authType == MXKAuthenticationTypeRegister)
{
_subTitleLabel.hidden = NO;
_subTitleLabel.text = [VectorL10n loginCreateAccount];
[_submitButton setTitle:[VectorL10n signUp] forState:UIControlStateNormal];
[_submitButton setTitle:[VectorL10n signUp] forState:UIControlStateHighlighted];
[_authSwitchButton setTitle:[VectorL10n back] forState:UIControlStateNormal];
[_authSwitchButton setTitle:[VectorL10n back] forState:UIControlStateHighlighted];
// Update supported authentication flow and associated information (defined in authentication session)
[self refreshAuthenticationSession];
self.screenTracker = [[AnalyticsScreenTracker alloc] initWithScreen:AnalyticsScreenRegister];
}
else if (authType == MXKAuthenticationTypeForgotPassword)
{
_subTitleLabel.hidden = YES;
if (isPasswordReseted)
{
[_submitButton setTitle:[VectorL10n back] forState:UIControlStateNormal];
[_submitButton setTitle:[VectorL10n back] forState:UIControlStateHighlighted];
}
else
{
[_submitButton setTitle:[VectorL10n submit] forState:UIControlStateNormal];
[_submitButton setTitle:[VectorL10n submit] forState:UIControlStateHighlighted];
[self refreshForgotPasswordSession];
}
[_authSwitchButton setTitle:[VectorL10n back] forState:UIControlStateNormal];
[_authSwitchButton setTitle:[VectorL10n back] forState:UIControlStateHighlighted];
self.screenTracker = [[AnalyticsScreenTracker alloc] initWithScreen:AnalyticsScreenForgotPassword];
}
if (self.isViewVisible)
{
[self.screenTracker trackScreen];
}
[self checkIdentityServer];
}
- (void)setAuthInputsView:(MXKAuthInputsView *)authInputsView
{
// Here a new view will be loaded, hide first subviews which depend on auth flow
_submitButton.hidden = YES;
_noFlowLabel.hidden = YES;
_retryButton.hidden = YES;
if (_authInputsView)
{
[_authInputsView removeObserver:self forKeyPath:@"viewHeightConstraint.constant"];
[NSLayoutConstraint deactivateConstraints:_authInputsView.constraints];
[_authInputsView removeFromSuperview];
_authInputsView.delegate = nil;
[_authInputsView destroy];
_authInputsView = nil;
}
_authInputsView = authInputsView;
CGFloat previousInputsContainerViewHeight = _authInputContainerViewHeightConstraint.constant;
if (_authInputsView)
{
_authInputsView.translatesAutoresizingMaskIntoConstraints = NO;
[_authInputsContainerView addSubview:_authInputsView];
_authInputsView.delegate = self;
_submitButton.hidden = NO;
_authInputsView.hidden = NO;
_authInputContainerViewHeightConstraint.constant = _authInputsView.viewHeightConstraint.constant;
NSLayoutConstraint* topConstraint = [NSLayoutConstraint constraintWithItem:_authInputsContainerView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:_authInputsView
attribute:NSLayoutAttributeTop
multiplier:1.0f
constant:0.0f];
NSLayoutConstraint* leadingConstraint = [NSLayoutConstraint constraintWithItem:_authInputsContainerView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:_authInputsView
attribute:NSLayoutAttributeLeading
multiplier:1.0f
constant:0.0f];
NSLayoutConstraint* trailingConstraint = [NSLayoutConstraint constraintWithItem:_authInputsContainerView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:_authInputsView
attribute:NSLayoutAttributeTrailing
multiplier:1.0f
constant:0.0f];
[NSLayoutConstraint activateConstraints:@[topConstraint, leadingConstraint, trailingConstraint]];
[_authInputsView addObserver:self forKeyPath:@"viewHeightConstraint.constant" options:0 context:nil];
}
else
{
// No input fields are displayed
_authInputContainerViewHeightConstraint.constant = _authInputContainerViewMinHeightConstraint.constant;
}
[self.view layoutIfNeeded];
// Refresh content view height by considering the updated height of inputs container
_contentViewHeightConstraint.constant += (_authInputContainerViewHeightConstraint.constant - previousInputsContainerViewHeight);
}
- (void)setDefaultHomeServerUrl:(NSString *)defaultHomeServerUrl
{
_defaultHomeServerUrl = defaultHomeServerUrl;
if (!_homeServerTextField.text.length)
{
[self setHomeServerTextFieldText:defaultHomeServerUrl];
}
}
- (void)setDefaultIdentityServerUrl:(NSString *)defaultIdentityServerUrl
{
_defaultIdentityServerUrl = defaultIdentityServerUrl;
if (!_identityServerTextField.text.length)
{
[self setIdentityServerTextFieldText:defaultIdentityServerUrl];
}
}
- (void)setHomeServerTextFieldText:(NSString *)homeServerUrl
{
if (!homeServerUrl.length)
{
// Force refresh with default value
homeServerUrl = _defaultHomeServerUrl;
}
_homeServerTextField.text = homeServerUrl;
if (!mxRestClient || ![mxRestClient.homeserver isEqualToString:homeServerUrl])
{
[self updateRESTClient];
if (_authType == MXKAuthenticationTypeLogin || _authType == MXKAuthenticationTypeRegister)
{
// Restore default UI
self.authType = _authType;
}
else
{
// Refresh the IS anyway
[self checkIdentityServer];
}
}
}
- (void)setIdentityServerTextFieldText:(NSString *)identityServerUrl
{
_identityServerTextField.text = identityServerUrl;
[self updateIdentityServerURL:identityServerUrl];
}
- (void)updateIdentityServerURL:(NSString*)url
{
if (![self.identityService.identityServer isEqualToString:url])
{
if (url.length)
{
self.identityService = [[MXIdentityService alloc] initWithIdentityServer:url accessToken:nil andHomeserverRestClient:mxRestClient];
}
else
{
self.identityService = nil;
}
}
[mxRestClient setIdentityServer:url.length ? url : nil];
}
- (void)setIdentityServerHidden:(BOOL)hidden
{
_identityServerContainer.hidden = hidden;
}
- (void)checkIdentityServer
{
[self cancelIdentityServerCheck];
// Hide the field while checking data
[self setIdentityServerHidden:YES];
NSString *homeserver = mxRestClient.homeserver;
// First, fetch the IS advertised by the HS
if (homeserver)
{
MXLogDebug(@"[MXKAuthenticationVC] checkIdentityServer for homeserver %@", homeserver);
autoDiscovery = [[MXAutoDiscovery alloc] initWithUrl:homeserver];
MXWeakify(self);
checkIdentityServerOperation = [autoDiscovery findClientConfig:^(MXDiscoveredClientConfig * _Nonnull discoveredClientConfig) {
MXStrongifyAndReturnIfNil(self);
NSString *identityServer = discoveredClientConfig.wellKnown.identityServer.baseUrl;
MXLogDebug(@"[MXKAuthenticationVC] checkIdentityServer: Identity server: %@", identityServer);
if (identityServer)
{
// Apply the provided IS
[self setIdentityServerTextFieldText:identityServer];
}
// Then, check if the HS needs an IS for running
MXWeakify(self);
MXHTTPOperation *operation = [self checkIdentityServerRequirementWithCompletion:^(BOOL identityServerRequired) {
MXStrongifyAndReturnIfNil(self);
self->checkIdentityServerOperation = nil;
// Show the field only if an IS is required so that the user can customise it
[self setIdentityServerHidden:!identityServerRequired];
}];
if (operation)
{
[self->checkIdentityServerOperation mutateTo:operation];
}
else
{
self->checkIdentityServerOperation = nil;
}
self->autoDiscovery = nil;
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
// No need to report this error to the end user
// There will be already an error about failing to get the auth flow from the HS
MXLogDebug(@"[MXKAuthenticationVC] checkIdentityServer. Error: %@", error);
self->autoDiscovery = nil;
}];
}
}
- (void)cancelIdentityServerCheck
{
if (checkIdentityServerOperation)
{
[checkIdentityServerOperation cancel];
checkIdentityServerOperation = nil;
}
}
- (MXHTTPOperation*)checkIdentityServerRequirementWithCompletion:(void (^)(BOOL identityServerRequired))completion
{
MXHTTPOperation *operation;
if (_authType == MXKAuthenticationTypeLogin)
{
// The identity server is only required for registration and password reset
// It is then stored in the user account data
completion(NO);
}
else
{
operation = [mxRestClient supportedMatrixVersions:^(MXMatrixVersions *matrixVersions) {
MXLogDebug(@"[MXKAuthenticationVC] checkIdentityServerRequirement: %@", matrixVersions.doesServerRequireIdentityServerParam ? @"YES": @"NO");
completion(matrixVersions.doesServerRequireIdentityServerParam);
} failure:^(NSError *error) {
// No need to report this error to the end user
// There will be already an error about failing to get the auth flow from the HS
MXLogDebug(@"[MXKAuthenticationVC] checkIdentityServerRequirement. Error: %@", error);
}];
}
return operation;
}
- (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled
{
_submitButton.enabled = (userInteractionEnabled && _authInputsView.areAllRequiredFieldsSet);
_authSwitchButton.enabled = userInteractionEnabled;
_homeServerTextField.enabled = userInteractionEnabled;
_identityServerTextField.enabled = userInteractionEnabled;
_userInteractionEnabled = userInteractionEnabled;
}
- (void)refreshAuthenticationSession
{
// Remove reachability observer
[[NSNotificationCenter defaultCenter] removeObserver:self name:AFNetworkingReachabilityDidChangeNotification object:nil];
// Cancel potential request in progress
[mxCurrentOperation cancel];
mxCurrentOperation = nil;
// Reset potential authentication fallback url
authenticationFallback = nil;
if (mxRestClient && (self.authType == MXKAuthenticationTypeLogin || self.authType == MXKAuthenticationTypeRegister))
{
MXWeakify(self);
// Get the login session to determine available SSO flows.
mxCurrentOperation = [mxRestClient getLoginSession:^(MXAuthenticationSession* loginAuthSession) {
MXStrongifyAndReturnIfNil(self);
if (self.authType == MXKAuthenticationTypeRegister)
{
MXWeakify(self);
self->mxCurrentOperation = [self->mxRestClient getRegisterSession:^(MXAuthenticationSession* registerAuthSession) {
MXStrongifyAndReturnIfNil(self);
// Handle the register session along with any SSO flows from the login session
MXLoginSSOFlow *loginSSOFlow = [self loginSSOFlowWithProvidersFromFlows:loginAuthSession.flows];
[self handleAuthenticationSession:registerAuthSession withFallbackSSOFlow:loginSSOFlow];
} failure:^(NSError *error) {
[self onFailureDuringMXOperation:error];
}];
}
else
{
// Handle the login session by itself
[self handleAuthenticationSession:loginAuthSession withFallbackSSOFlow:nil];
}
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
[self onFailureDuringMXOperation:error];
}];
}
else
{
// Not supported for other types
MXLogDebug(@"[MXKAuthenticationVC] refreshAuthenticationSession is ignored");
}
}
- (void)handleAuthenticationSession:(MXAuthenticationSession *)authSession withFallbackSSOFlow:(MXLoginSSOFlow *)fallbackSSOFlow
{
mxCurrentOperation = nil;
[_authenticationActivityIndicator stopAnimating];
// Check whether fallback is defined, and retrieve the right input view class.
Class authInputsViewClass;
if (_authType == MXKAuthenticationTypeLogin)
{
authenticationFallback = [mxRestClient loginFallback];
authInputsViewClass = loginAuthInputsViewClass;
}
else if (_authType == MXKAuthenticationTypeRegister)
{
authenticationFallback = [mxRestClient registerFallback];
authInputsViewClass = registerAuthInputsViewClass;
}
else
{
// Not supported for other types
MXLogDebug(@"[MXKAuthenticationVC] handleAuthenticationSession is ignored");
return;
}
MXKAuthInputsView *authInputsView = nil;
if (authInputsViewClass)
{
// Instantiate a new auth inputs view, except if the current one is already an instance of this class.
if (self.authInputsView && self.authInputsView.class == authInputsViewClass)
{
// Use the current view
authInputsView = self.authInputsView;
}
else
{
authInputsView = [authInputsViewClass authInputsView];
}
}
if (authInputsView)
{
// Apply authentication session on inputs view
if ([authInputsView setAuthSession:authSession withAuthType:_authType] == NO)
{
MXLogDebug(@"[MXKAuthenticationVC] Received authentication settings are not supported");
authInputsView = nil;
}
else if (!_softLogoutCredentials)
{
// If all listed flows in this authentication session are not supported we suggest using the fallback page.
if (authenticationFallback.length && authInputsView.authSession.flows.count == 0)
{
MXLogDebug(@"[MXKAuthenticationVC] No supported flow, suggest using fallback page");
authInputsView = nil;
}
else if (authInputsView.authSession.flows.count != authSession.flows.count)
{
MXLogDebug(@"[MXKAuthenticationVC] The authentication session contains at least one unsupported flow");
}
}
}
if (authInputsView)
{
// Check whether the current view must be replaced
if (self.authInputsView != authInputsView)
{
// Refresh layout
self.authInputsView = authInputsView;
}
// Refresh user interaction
self.userInteractionEnabled = _userInteractionEnabled;
// Check whether an external set of parameters have been defined to pursue a registration
if (self.externalRegistrationParameters)
{
if ([authInputsView setExternalRegistrationParameters:self.externalRegistrationParameters])
{
// Launch authentication now
[self onButtonPressed:_submitButton];
}
else
{
[self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[VectorL10n notSupportedYet]}]];
_externalRegistrationParameters = nil;
// Restore login screen on failure
self.authType = MXKAuthenticationTypeLogin;
}
}
if (_softLogoutCredentials)
{
[authInputsView setSoftLogoutCredentials:_softLogoutCredentials];
}
}
else
{
// Remove the potential auth inputs view
self.authInputsView = nil;
// Cancel external registration parameters if any
_externalRegistrationParameters = nil;
// Notify user that no flow is supported
if (_authType == MXKAuthenticationTypeLogin)
{
_noFlowLabel.text = [VectorL10n loginErrorDoNotSupportLoginFlows];
}
else
{
_noFlowLabel.text = [VectorL10n loginErrorRegistrationIsNotSupported];
}
MXLogDebug(@"[MXKAuthenticationVC] Warning: %@", _noFlowLabel.text);
if (authenticationFallback.length)
{
[_retryButton setTitle:[VectorL10n loginUseFallback] forState:UIControlStateNormal];
[_retryButton setTitle:[VectorL10n loginUseFallback] forState:UIControlStateNormal];
}
else
{
[_retryButton setTitle:[VectorL10n retry] forState:UIControlStateNormal];
[_retryButton setTitle:[VectorL10n retry] forState:UIControlStateNormal];
}
_noFlowLabel.hidden = NO;
_retryButton.hidden = NO;
}
}
- (void)setExternalRegistrationParameters:(NSDictionary*)parameters
{
if (parameters.count)
{
MXLogDebug(@"[MXKAuthenticationVC] setExternalRegistrationParameters");
// Cancel the current operation if any.
[self cancel];
// Load the view controllers view if it has not yet been loaded.
// This is required before updating view's textfields (homeserver url...)
[self loadViewIfNeeded];
// Force register mode
self.authType = MXKAuthenticationTypeRegister;
// Apply provided homeserver if any
id hs_url = parameters[@"hs_url"];
NSString *homeserverURL = nil;
if (hs_url && [hs_url isKindOfClass:NSString.class])
{
homeserverURL = hs_url;
}
[self setHomeServerTextFieldText:homeserverURL];
// Apply provided identity server if any
id is_url = parameters[@"is_url"];
NSString *identityURL = nil;
if (is_url && [is_url isKindOfClass:NSString.class])
{
identityURL = is_url;
}
[self setIdentityServerTextFieldText:identityURL];
// Disable user interaction
self.userInteractionEnabled = NO;
// Cancel potential request in progress
[mxCurrentOperation cancel];
mxCurrentOperation = nil;
// Remove the current auth inputs view
self.authInputsView = nil;
// Set external parameters and trigger a refresh (the parameters will be taken into account during [handleAuthenticationSession:])
_externalRegistrationParameters = parameters;
[self refreshAuthenticationSession];
}
else
{
MXLogDebug(@"[MXKAuthenticationVC] reset externalRegistrationParameters");
_externalRegistrationParameters = nil;
// Restore default UI
self.authType = _authType;
}
}
- (void)setSoftLogoutCredentials:(MXCredentials *)softLogoutCredentials
{
MXLogDebug(@"[MXKAuthenticationVC] setSoftLogoutCredentials");
// Cancel the current operation if any.
[self cancel];
// Load the view controllers view if it has not yet been loaded.
// This is required before updating view's textfields (homeserver url...)
[self loadViewIfNeeded];
if (softLogoutCredentials)
{
// Force register mode
self.authType = MXKAuthenticationTypeLogin;
[self setHomeServerTextFieldText:softLogoutCredentials.homeServer];
[self setIdentityServerTextFieldText:softLogoutCredentials.identityServer];
// Cancel potential request in progress
[mxCurrentOperation cancel];
mxCurrentOperation = nil;
// Remove the current auth inputs view
self.authInputsView = nil;
}
// Set parameters and trigger a refresh (the parameters will be taken into account during [handleAuthenticationSession:])
_softLogoutCredentials = softLogoutCredentials;
[self refreshAuthenticationSession];
}
- (void)setOnUnrecognizedCertificateBlock:(MXHTTPClientOnUnrecognizedCertificate)onUnrecognizedCertificateBlock
{
onUnrecognizedCertificateCustomBlock = onUnrecognizedCertificateBlock;
}
- (void)isUserNameInUse:(void (^)(BOOL isUserNameInUse))callback
{
mxCurrentOperation = [mxRestClient isUserNameInUse:self.authInputsView.userId callback:^(BOOL isUserNameInUse) {
self->mxCurrentOperation = nil;
if (callback)
{
callback (isUserNameInUse);
}
}];
}
- (void)testUserRegistration:(void (^)(MXError *mxError))callback
{
mxCurrentOperation = [mxRestClient testUserRegistration:self.authInputsView.userId callback:callback];
}
- (MXLoginSSOFlow*)loginSSOFlowWithProvidersFromFlows:(NSArray<MXLoginFlow*>*)loginFlows
{
MXLoginSSOFlow *ssoFlowWithProviders;
for (MXLoginFlow *loginFlow in loginFlows)
{
if ([loginFlow isKindOfClass:MXLoginSSOFlow.class])
{
MXLoginSSOFlow *ssoFlow = (MXLoginSSOFlow *)loginFlow;
if (ssoFlow.identityProviders.count)
{
ssoFlowWithProviders = ssoFlow;
break;
}
}
}
return ssoFlowWithProviders;
}
- (IBAction)onButtonPressed:(id)sender
{
[self dismissKeyboard];
if (sender == _submitButton)
{
// Disable user interaction to prevent multiple requests
self.userInteractionEnabled = NO;
// Check parameters validity
NSString *errorMsg = [self.authInputsView validateParameters];
if (errorMsg)
{
[self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:errorMsg}]];
}
else
{
[self.authInputsContainerView bringSubviewToFront: _authenticationActivityIndicator];
// Launch the authentication according to its type
if (_authType == MXKAuthenticationTypeLogin)
{
// Prepare the parameters dict
[self.authInputsView prepareParameters:^(NSDictionary *parameters, NSError *error) {
if (parameters && self->mxRestClient)
{
[self->_authenticationActivityIndicator startAnimating];
[self loginWithParameters:parameters];
}
else
{
MXLogDebug(@"[MXKAuthenticationVC] Failed to prepare parameters");
[self onFailureDuringAuthRequest:error];
}
}];
}
else if (_authType == MXKAuthenticationTypeRegister)
{
// Check here the availability of the userId
if (self.authInputsView.userId.length)
{
[_authenticationActivityIndicator startAnimating];
if (self.authInputsView.password.length)
{
// Trigger here a register request in order to associate the filled userId and password to the current session id
// This will check the availability of the userId at the same time
NSDictionary *parameters = @{@"auth": @{
@"session": self.authInputsView.authSession.session,
@"type": kMXLoginFlowTypeDummy
},
@"username": self.authInputsView.userId,
@"password": self.authInputsView.password,
@"bind_email": @(NO),
@"initial_device_display_name":self.deviceDisplayName
};
mxCurrentOperation = [mxRestClient registerWithParameters:parameters success:^(NSDictionary *JSONResponse) {
// Unexpected case where the registration succeeds without any other stages
MXLoginResponse *loginResponse;
MXJSONModelSetMXJSONModel(loginResponse, MXLoginResponse, JSONResponse);
MXCredentials *credentials = [[MXCredentials alloc] initWithLoginResponse:loginResponse
andDefaultCredentials:self->mxRestClient.credentials];
// Sanity check
if (!credentials.userId || !credentials.accessToken)
{
[self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[VectorL10n notSupportedYet]}]];
}
else
{
MXLogDebug(@"[MXKAuthenticationVC] Registration succeeded");
// Report the certificate trusted by user (if any)
credentials.allowedCertificate = self->mxRestClient.allowedCertificate;
[self onSuccessfulLogin:credentials];
}
} failure:^(NSError *error) {
self->mxCurrentOperation = nil;
// An updated authentication session should be available in response data in case of unauthorized request.
NSDictionary *JSONResponse = nil;
if (error.userInfo[MXHTTPClientErrorResponseDataKey])
{
JSONResponse = error.userInfo[MXHTTPClientErrorResponseDataKey];
}
if (JSONResponse)
{
MXAuthenticationSession *authSession = [MXAuthenticationSession modelFromJSON:JSONResponse];
[self->_authenticationActivityIndicator stopAnimating];
// Update session identifier
self.authInputsView.authSession.session = authSession.session;
// Launch registration by preparing parameters dict
[self.authInputsView prepareParameters:^(NSDictionary *parameters, NSError *error) {
if (parameters && self->mxRestClient)
{
[self->_authenticationActivityIndicator startAnimating];
[self registerWithParameters:parameters];
}
else
{
MXLogDebug(@"[MXKAuthenticationVC] Failed to prepare parameters");
[self onFailureDuringAuthRequest:error];
}
}];
}
else
{
[self onFailureDuringAuthRequest:error];
}
}];
}
else
{
[self isUserNameInUse:^(BOOL isUserNameInUse) {
if (isUserNameInUse)
{
MXLogDebug(@"[MXKAuthenticationVC] User name is already use");
[self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[VectorL10n authUsernameInUse]}]];
}
else
{
[self->_authenticationActivityIndicator stopAnimating];
// Launch registration by preparing parameters dict
[self.authInputsView prepareParameters:^(NSDictionary *parameters, NSError *error) {
if (parameters && self->mxRestClient)
{
[self->_authenticationActivityIndicator startAnimating];
[self registerWithParameters:parameters];
}
else
{
MXLogDebug(@"[MXKAuthenticationVC] Failed to prepare parameters");
[self onFailureDuringAuthRequest:error];
}
}];
}
}];
}
}
else if (self.externalRegistrationParameters)
{
// Launch registration by preparing parameters dict
[self.authInputsView prepareParameters:^(NSDictionary *parameters, NSError *error) {
if (parameters && self->mxRestClient)
{
[self->_authenticationActivityIndicator startAnimating];
[self registerWithParameters:parameters];
}
else
{
MXLogDebug(@"[MXKAuthenticationVC] Failed to prepare parameters");
[self onFailureDuringAuthRequest:error];
}
}];
}
else
{
MXLogDebug(@"[MXKAuthenticationVC] User name is missing");
[self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[VectorL10n authInvalidUserName]}]];
}
}
else if (_authType == MXKAuthenticationTypeForgotPassword)
{
// Check whether the password has been reseted
if (isPasswordReseted)
{
// Return to login screen
self.authType = MXKAuthenticationTypeLogin;
}
else
{
// Prepare the parameters dict
[self.authInputsView prepareParameters:^(NSDictionary *parameters, NSError *error) {
if (parameters && self->mxRestClient)
{
[self->_authenticationActivityIndicator startAnimating];
[self resetPasswordWithParameters:parameters];
}
else
{
MXLogDebug(@"[MXKAuthenticationVC] Failed to prepare parameters");
[self onFailureDuringAuthRequest:error];
}
}];
}
}
}
}
else if (sender == _authSwitchButton)
{
if (_authType == MXKAuthenticationTypeLogin)
{
self.authType = MXKAuthenticationTypeRegister;
}
else
{
self.authType = MXKAuthenticationTypeLogin;
}
}
else if (sender == _retryButton)
{
if (authenticationFallback)
{
[self showAuthenticationFallBackView:authenticationFallback];
}
else
{
[self refreshAuthenticationSession];
}
}
else if (sender == _cancelAuthFallbackButton)
{
// Hide fallback webview
[self hideRegistrationFallbackView];
}
}
- (void)cancel
{
MXLogDebug(@"[MXKAuthenticationVC] cancel");
// Cancel external registration parameters if any
_externalRegistrationParameters = nil;
if (registrationTimer)
{
[registrationTimer invalidate];
registrationTimer = nil;
}
// Cancel request in progress
if (mxCurrentOperation)
{
[mxCurrentOperation cancel];
mxCurrentOperation = nil;
}
[_authenticationActivityIndicator stopAnimating];
self.userInteractionEnabled = YES;
// Reset potential completed stages
self.authInputsView.authSession.completed = nil;
// Update authentication inputs view to return in initial step
[self.authInputsView setAuthSession:self.authInputsView.authSession withAuthType:_authType];
}
- (void)onFailureDuringAuthRequest:(NSError *)error
{
mxCurrentOperation = nil;
[_authenticationActivityIndicator stopAnimating];
self.userInteractionEnabled = YES;
// Ignore connection cancellation error
if (([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled))
{
MXLogDebug(@"[MXKAuthenticationVC] Auth request cancelled");
return;
}
MXLogDebug(@"[MXKAuthenticationVC] Auth request failed: %@", error);
// Cancel external registration parameters if any
_externalRegistrationParameters = nil;
// Translate the error code to a human message
NSString *title = error.localizedFailureReason;
if (!title)
{
if (self.authType == MXKAuthenticationTypeLogin)
{
title = [VectorL10n loginErrorTitle];
}
else if (self.authType == MXKAuthenticationTypeRegister)
{
title = [VectorL10n registerErrorTitle];
}
else
{
title = [VectorL10n error];
}
}
NSString* message = error.localizedDescription;
NSDictionary* dict = error.userInfo;
// detect if it is a Matrix SDK issue
if (dict)
{
NSString* localizedError = [dict valueForKey:@"error"];
NSString* errCode = [dict valueForKey:@"errcode"];
if (localizedError.length > 0)
{
message = localizedError;
}
if (errCode)
{
if ([errCode isEqualToString:kMXErrCodeStringForbidden])
{
message = [VectorL10n loginErrorForbidden];
}
else if ([errCode isEqualToString:kMXErrCodeStringUnknownToken])
{
message = [VectorL10n loginErrorUnknownToken];
}
else if ([errCode isEqualToString:kMXErrCodeStringBadJSON])
{
message = [VectorL10n loginErrorBadJson];
}
else if ([errCode isEqualToString:kMXErrCodeStringNotJSON])
{
message = [VectorL10n loginErrorNotJson];
}
else if ([errCode isEqualToString:kMXErrCodeStringLimitExceeded])
{
message = [VectorL10n loginErrorLimitExceeded];
}
else if ([errCode isEqualToString:kMXErrCodeStringUserInUse])
{
message = [VectorL10n loginErrorUserInUse];
}
else if ([errCode isEqualToString:kMXErrCodeStringLoginEmailURLNotYet])
{
message = [VectorL10n loginErrorLoginEmailNotYet];
}
else if ([errCode isEqualToString:kMXErrCodeStringResourceLimitExceeded])
{
[self showResourceLimitExceededError:dict onAdminContactTapped:nil];
return;
}
else if (!message.length)
{
message = errCode;
}
}
}
// Alert user
if (alert)
{
[alert dismissViewControllerAnimated:NO completion:nil];
}
alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
self->alert = nil;
}]];
[self presentViewController:alert animated:YES completion:nil];
// Update authentication inputs view to return in initial step
[self.authInputsView setAuthSession:self.authInputsView.authSession withAuthType:_authType];
if (self.softLogoutCredentials)
{
self.authInputsView.softLogoutCredentials = self.softLogoutCredentials;
}
}
- (void)showResourceLimitExceededError:(NSDictionary *)errorDict onAdminContactTapped:(void (^)(NSURL *adminContact))onAdminContactTapped
{
mxCurrentOperation = nil;
[_authenticationActivityIndicator stopAnimating];
self.userInteractionEnabled = YES;
// Alert user
if (alert)
{
[alert dismissViewControllerAnimated:NO completion:nil];
}
// Parse error data
NSString *limitType, *adminContactString;
NSURL *adminContact;
MXJSONModelSetString(limitType, errorDict[kMXErrorResourceLimitExceededLimitTypeKey]);
MXJSONModelSetString(adminContactString, errorDict[kMXErrorResourceLimitExceededAdminContactKey]);
if (adminContactString)
{
adminContact = [NSURL URLWithString:adminContactString];
}
NSString *title = [VectorL10n loginErrorResourceLimitExceededTitle];
// Build the message content
NSMutableString *message = [NSMutableString new];
if ([limitType isEqualToString:kMXErrorResourceLimitExceededLimitTypeMonthlyActiveUserValue])
{
[message appendString:[VectorL10n loginErrorResourceLimitExceededMessageMonthlyActiveUser]];
}
else
{
[message appendString:[VectorL10n loginErrorResourceLimitExceededMessageDefault]];
}
[message appendString:[VectorL10n loginErrorResourceLimitExceededMessageContact]];
// Build the alert
alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
MXWeakify(self);
if (adminContact && onAdminContactTapped)
{
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n loginErrorResourceLimitExceededContactButton]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
MXStrongifyAndReturnIfNil(self);
self->alert = nil;
// Let the system handle the URI
// It could be something like "mailto: server.admin@example.com"
onAdminContactTapped(adminContact);
}]];
}
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
MXStrongifyAndReturnIfNil(self);
self->alert = nil;
}]];
[self presentViewController:alert animated:YES completion:nil];
// Update authentication inputs view to return in initial step
[self.authInputsView setAuthSession:self.authInputsView.authSession withAuthType:_authType];
}
- (void)onSuccessfulLogin:(MXCredentials*)credentials
{
mxCurrentOperation = nil;
[_authenticationActivityIndicator stopAnimating];
self.userInteractionEnabled = YES;
if (self.softLogoutCredentials)
{
// Hydrate the account with the new access token
MXKAccount *account = [[MXKAccountManager sharedManager] accountForUserId:self.softLogoutCredentials.userId];
[[MXKAccountManager sharedManager] hydrateAccount:account withCredentials:credentials];
if (_delegate)
{
[_delegate authenticationViewController:self didLogWithUserId:credentials.userId];
}
}
// Sanity check: check whether the user is not already logged in with this id
else if ([[MXKAccountManager sharedManager] accountForUserId:credentials.userId])
{
//Alert user
__weak typeof(self) weakSelf = self;
if (alert)
{
[alert dismissViewControllerAnimated:NO completion:nil];
}
alert = [UIAlertController alertControllerWithTitle:[VectorL10n loginErrorAlreadyLoggedIn] message:nil preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
// We remove the authentication view controller.
typeof(self) self = weakSelf;
self->alert = nil;
[self withdrawViewControllerAnimated:YES completion:nil];
}]];
[self presentViewController:alert animated:YES completion:nil];
}
else
{
// Report the new account in account manager
if (!credentials.identityServer)
{
credentials.identityServer = _identityServerTextField.text;
}
[self createAccountWithCredentials:credentials];
}
}
- (MXHTTPOperation *)currentHttpOperation
{
return mxCurrentOperation;
}
#pragma mark - Privates
// Hook point for triggering device rehydration in subclasses
// Avoid cycles by using a separate private method do to the actual work
- (void)createAccountWithCredentials:(MXCredentials *)credentials
{
[self _createAccountWithCredentials:credentials];
}
- (void)_createAccountWithCredentials:(MXCredentials *)credentials
{
MXKAccount *account = [[MXKAccount alloc] initWithCredentials:credentials];
account.identityServerURL = credentials.identityServer;
[[MXKAccountManager sharedManager] addAccount:account andOpenSession:YES];
if (_delegate)
{
[_delegate authenticationViewController:self didLogWithUserId:credentials.userId];
}
}
- (NSString *)deviceDisplayName
{
if (_deviceDisplayName)
{
return _deviceDisplayName;
}
#if TARGET_OS_IPHONE
NSString *deviceName = [[UIDevice currentDevice].model isEqualToString:@"iPad"] ? [VectorL10n loginTabletDevice] : [VectorL10n loginMobileDevice];
#elif TARGET_OS_OSX
NSString *deviceName = [VectorL10n loginDesktopDevice];
#endif
return deviceName;
}
- (void)refreshForgotPasswordSession
{
[_authenticationActivityIndicator stopAnimating];
MXKAuthInputsView *authInputsView = nil;
if (forgotPasswordAuthInputsViewClass)
{
// Instantiate a new auth inputs view, except if the current one is already an instance of this class.
if (self.authInputsView && self.authInputsView.class == forgotPasswordAuthInputsViewClass)
{
// Use the current view
authInputsView = self.authInputsView;
}
else
{
authInputsView = [forgotPasswordAuthInputsViewClass authInputsView];
}
}
if (authInputsView)
{
// Update authentication inputs view to return in initial step
[authInputsView setAuthSession:nil withAuthType:MXKAuthenticationTypeForgotPassword];
// Check whether the current view must be replaced
if (self.authInputsView != authInputsView)
{
// Refresh layout
self.authInputsView = authInputsView;
}
// Refresh user interaction
self.userInteractionEnabled = _userInteractionEnabled;
}
else
{
// Remove the potential auth inputs view
self.authInputsView = nil;
_noFlowLabel.text = [VectorL10n loginErrorForgotPasswordIsNotSupported];
MXLogDebug(@"[MXKAuthenticationVC] Warning: %@", _noFlowLabel.text);
_noFlowLabel.hidden = NO;
}
}
- (void)updateRESTClient
{
NSString *homeserverURL = [HomeserverAddress sanitized:_homeServerTextField.text];
if (homeserverURL.length)
{
// Check change
if ([homeserverURL isEqualToString:mxRestClient.homeserver] == NO)
{
mxRestClient = [[MXRestClient alloc] initWithHomeServer:homeserverURL andOnUnrecognizedCertificateBlock:^BOOL(NSData *certificate) {
// Check first if the app developer provided its own certificate handler.
if (self->onUnrecognizedCertificateCustomBlock)
{
return self->onUnrecognizedCertificateCustomBlock (certificate);
}
// Else prompt the user by displaying a fingerprint (SHA256) of the certificate.
__block BOOL isTrusted;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSString *title = [VectorL10n sslCouldNotVerify];
NSString *homeserverURLStr = [VectorL10n sslHomeserverUrl:homeserverURL];
NSString *fingerprint = [VectorL10n sslFingerprintHash:@"SHA256"];
NSString *certFingerprint = [certificate mx_SHA256AsHexString];
NSString *msg = [NSString stringWithFormat:@"%@\n\n%@\n\n%@\n\n%@\n\n%@\n\n%@", [VectorL10n sslCertNotTrust], [VectorL10n sslCertNewAccountExpl], homeserverURLStr, fingerprint, certFingerprint, [VectorL10n sslOnlyAccept]];
if (self->alert)
{
[self->alert dismissViewControllerAnimated:NO completion:nil];
}
self->alert = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert];
[self->alert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
self->alert = nil;
isTrusted = NO;
dispatch_semaphore_signal(semaphore);
}]];
[self->alert addAction:[UIAlertAction actionWithTitle:[VectorL10n sslTrust]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
self->alert = nil;
isTrusted = YES;
dispatch_semaphore_signal(semaphore);
}]];
dispatch_async(dispatch_get_main_queue(), ^{
[self presentViewController:self->alert animated:YES completion:nil];
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (!isTrusted)
{
// Cancel request in progress
[self->mxCurrentOperation cancel];
self->mxCurrentOperation = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self name:AFNetworkingReachabilityDidChangeNotification object:nil];
[self->_authenticationActivityIndicator stopAnimating];
}
return isTrusted;
}];
if (_identityServerTextField.text.length)
{
[self updateIdentityServerURL:self.identityServerTextField.text];
}
}
}
else
{
[mxRestClient close];
mxRestClient = nil;
}
}
- (void)loginWithParameters:(NSDictionary*)parameters
{
// Add the device name
NSMutableDictionary *theParameters = [NSMutableDictionary dictionaryWithDictionary:parameters];
theParameters[@"initial_device_display_name"] = self.deviceDisplayName;
mxCurrentOperation = [mxRestClient login:theParameters success:^(NSDictionary *JSONResponse) {
MXLoginResponse *loginResponse;
MXJSONModelSetMXJSONModel(loginResponse, MXLoginResponse, JSONResponse);
MXCredentials *credentials = [[MXCredentials alloc] initWithLoginResponse:loginResponse
andDefaultCredentials:self->mxRestClient.credentials];
// Sanity check
if (!credentials.userId || !credentials.accessToken)
{
[self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[VectorL10n notSupportedYet]}]];
}
else
{
MXLogDebug(@"[MXKAuthenticationVC] Login process succeeded");
// Report the certificate trusted by user (if any)
credentials.allowedCertificate = self->mxRestClient.allowedCertificate;
[self onSuccessfulLogin:credentials];
}
} failure:^(NSError *error) {
[self onFailureDuringAuthRequest:error];
}];
}
- (void)registerWithParameters:(NSDictionary*)parameters
{
if (registrationTimer)
{
[registrationTimer invalidate];
registrationTimer = nil;
}
// Add the device name
NSMutableDictionary *theParameters = [NSMutableDictionary dictionaryWithDictionary:parameters];
theParameters[@"initial_device_display_name"] = self.deviceDisplayName;
mxCurrentOperation = [mxRestClient registerWithParameters:theParameters success:^(NSDictionary *JSONResponse) {
MXLoginResponse *loginResponse;
MXJSONModelSetMXJSONModel(loginResponse, MXLoginResponse, JSONResponse);
MXCredentials *credentials = [[MXCredentials alloc] initWithLoginResponse:loginResponse
andDefaultCredentials:self->mxRestClient.credentials];
// Sanity check
if (!credentials.userId || !credentials.accessToken)
{
[self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[VectorL10n notSupportedYet]}]];
}
else
{
MXLogDebug(@"[MXKAuthenticationVC] Registration succeeded");
// Report the certificate trusted by user (if any)
credentials.allowedCertificate = self->mxRestClient.allowedCertificate;
[self onSuccessfulLogin:credentials];
}
} failure:^(NSError *error) {
self->mxCurrentOperation = nil;
// Check whether the authentication is pending (for example waiting for email validation)
MXError *mxError = [[MXError alloc] initWithNSError:error];
if (mxError && [mxError.errcode isEqualToString:kMXErrCodeStringUnauthorized])
{
MXLogDebug(@"[MXKAuthenticationVC] Wait for email validation");
// Postpone a new attempt in 10 sec
self->registrationTimer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(registrationTimerFireMethod:) userInfo:parameters repeats:NO];
}
else
{
// The completed stages should be available in response data in case of unauthorized request.
NSDictionary *JSONResponse = nil;
if (error.userInfo[MXHTTPClientErrorResponseDataKey])
{
JSONResponse = error.userInfo[MXHTTPClientErrorResponseDataKey];
}
if (JSONResponse)
{
MXAuthenticationSession *authSession = [MXAuthenticationSession modelFromJSON:JSONResponse];
if (authSession.completed)
{
[self->_authenticationActivityIndicator stopAnimating];
// Update session identifier in case of change
self.authInputsView.authSession.session = authSession.session;
[self.authInputsView updateAuthSessionWithCompletedStages:authSession.completed didUpdateParameters:^(NSDictionary *parameters, NSError *error) {
if (parameters)
{
MXLogDebug(@"[MXKAuthenticationVC] Pursue registration");
[self->_authenticationActivityIndicator startAnimating];
[self registerWithParameters:parameters];
}
else
{
MXLogDebug(@"[MXKAuthenticationVC] Failed to update parameters");
[self onFailureDuringAuthRequest:error];
}
}];
return;
}
[self onFailureDuringAuthRequest:[NSError errorWithDomain:MXKAuthErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:[VectorL10n notSupportedYet]}]];
}
else
{
[self onFailureDuringAuthRequest:error];
}
}
}];
}
- (void)registrationTimerFireMethod:(NSTimer *)timer
{
if (timer == registrationTimer && timer.isValid)
{
MXLogDebug(@"[MXKAuthenticationVC] Retry registration");
[self registerWithParameters:registrationTimer.userInfo];
}
}
- (void)resetPasswordWithParameters:(NSDictionary*)parameters
{
mxCurrentOperation = [mxRestClient resetPasswordWithParameters:parameters success:^() {
MXLogDebug(@"[MXKAuthenticationVC] Reset password succeeded");
self->mxCurrentOperation = nil;
[self->_authenticationActivityIndicator stopAnimating];
self->isPasswordReseted = YES;
// Force UI update to refresh submit button title.
self.authType = self->_authType;
// Refresh the authentication inputs view on success.
[self.authInputsView nextStep];
} failure:^(NSError *error) {
MXError *mxError = [[MXError alloc] initWithNSError:error];
if (mxError && [mxError.errcode isEqualToString:kMXErrCodeStringUnauthorized])
{
MXLogDebug(@"[MXKAuthenticationVC] Forgot Password: wait for email validation");
self->mxCurrentOperation = nil;
[self->_authenticationActivityIndicator stopAnimating];
if (self->alert)
{
[self->alert dismissViewControllerAnimated:NO completion:nil];
}
self->alert = [UIAlertController alertControllerWithTitle:[VectorL10n error] message:[VectorL10n authResetPasswordErrorUnauthorized] preferredStyle:UIAlertControllerStyleAlert];
[self->alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
self->alert = nil;
}]];
[self presentViewController:self->alert animated:YES completion:nil];
}
else if (mxError && [mxError.errcode isEqualToString:kMXErrCodeStringNotFound])
{
MXLogDebug(@"[MXKAuthenticationVC] Forgot Password: not found");
NSMutableDictionary *userInfo;
if (error.userInfo)
{
userInfo = [NSMutableDictionary dictionaryWithDictionary:error.userInfo];
}
else
{
userInfo = [NSMutableDictionary dictionary];
}
userInfo[NSLocalizedDescriptionKey] = [VectorL10n authResetPasswordErrorNotFound];
[self onFailureDuringAuthRequest:[NSError errorWithDomain:kMXNSErrorDomain code:0 userInfo:userInfo]];
}
else
{
[self onFailureDuringAuthRequest:error];
}
}];
}
- (void)onFailureDuringMXOperation:(NSError*)error
{
mxCurrentOperation = nil;
[_authenticationActivityIndicator stopAnimating];
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled)
{
// Ignore this error
MXLogDebug(@"[MXKAuthenticationVC] flows request cancelled");
return;
}
MXLogDebug(@"[MXKAuthenticationVC] Failed to get %@ flows: %@", (_authType == MXKAuthenticationTypeLogin ? @"Login" : @"Register"), error);
// Cancel external registration parameters if any
_externalRegistrationParameters = nil;
// Alert user
NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey];
if (!title)
{
title = [VectorL10n error];
}
NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey];
if (alert)
{
[alert dismissViewControllerAnimated:NO completion:nil];
}
alert = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n dismiss]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
self->alert = nil;
}]];
[self presentViewController:alert animated:YES completion:nil];
// Handle specific error code here
if ([error.domain isEqualToString:NSURLErrorDomain])
{
// Check network reachability
if (error.code == NSURLErrorNotConnectedToInternet)
{
// Add reachability observer in order to launch a new request when network will be available
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onReachabilityStatusChange:) name:AFNetworkingReachabilityDidChangeNotification object:nil];
}
else if (error.code == kCFURLErrorTimedOut)
{
// Send a new request in 2 sec
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self refreshAuthenticationSession];
});
}
else
{
// Remove the potential auth inputs view
self.authInputsView = nil;
}
}
else
{
// Remove the potential auth inputs view
self.authInputsView = nil;
}
if (!_authInputsView)
{
// Display failure reason
_noFlowLabel.hidden = NO;
_noFlowLabel.text = [error.userInfo valueForKey:NSLocalizedDescriptionKey];
if (!_noFlowLabel.text.length)
{
_noFlowLabel.text = [VectorL10n loginErrorNoLoginFlow];
}
[_retryButton setTitle:[VectorL10n retry] forState:UIControlStateNormal];
[_retryButton setTitle:[VectorL10n retry] forState:UIControlStateNormal];
_retryButton.hidden = NO;
}
}
- (void)onReachabilityStatusChange:(NSNotification *)notif
{
AFNetworkReachabilityManager *reachabilityManager = [AFNetworkReachabilityManager sharedManager];
AFNetworkReachabilityStatus status = reachabilityManager.networkReachabilityStatus;
if (status == AFNetworkReachabilityStatusReachableViaWiFi || status == AFNetworkReachabilityStatusReachableViaWWAN)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self refreshAuthenticationSession];
});
}
else if (status == AFNetworkReachabilityStatusNotReachable)
{
_noFlowLabel.text = [VectorL10n networkErrorNotReachable];
}
}
#pragma mark - Keyboard handling
- (void)dismissKeyboard
{
// Hide the keyboard
[_authInputsView dismissKeyboard];
[_homeServerTextField resignFirstResponder];
[_identityServerTextField resignFirstResponder];
}
#pragma mark - UITextField delegate
- (void)onTextFieldChange:(NSNotification *)notif
{
_submitButton.enabled = _authInputsView.areAllRequiredFieldsSet;
if (notif.object == _homeServerTextField)
{
// If any, the current request is obsolete
[self cancelIdentityServerCheck];
[self setIdentityServerHidden:YES];
}
}
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
if (textField == _homeServerTextField)
{
// Cancel supported AuthFlow refresh if a request is in progress
[[NSNotificationCenter defaultCenter] removeObserver:self name:AFNetworkingReachabilityDidChangeNotification object:nil];
if (mxCurrentOperation)
{
// Cancel potential request in progress
[mxCurrentOperation cancel];
mxCurrentOperation = nil;
}
}
return YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
if (textField == _homeServerTextField)
{
[self setHomeServerTextFieldText:textField.text];
}
else if (textField == _identityServerTextField)
{
[self setIdentityServerTextFieldText:textField.text];
}
}
- (BOOL)textFieldShouldReturn:(UITextField*)textField
{
if (textField.returnKeyType == UIReturnKeyDone)
{
// "Done" key has been pressed
[textField resignFirstResponder];
}
return YES;
}
#pragma mark - AuthInputsViewDelegate delegate
- (void)authInputsView:(MXKAuthInputsView*)authInputsView presentAlertController:(UIAlertController*)inputsAlert
{
[self dismissKeyboard];
[self presentViewController:inputsAlert animated:YES completion:nil];
}
- (void)authInputsViewDidPressDoneKey:(MXKAuthInputsView *)authInputsView
{
if (_submitButton.isEnabled)
{
// Launch authentication now
[self onButtonPressed:_submitButton];
}
}
- (MXRestClient *)authInputsViewThirdPartyIdValidationRestClient:(MXKAuthInputsView *)authInputsView
{
return mxRestClient;
}
- (MXIdentityService *)authInputsViewThirdPartyIdValidationIdentityService:(MXIdentityService *)authInputsView
{
return self.identityService;
}
#pragma mark - Authentication Fallback
- (void)showAuthenticationFallBackView
{
[self showAuthenticationFallBackView:authenticationFallback];
}
- (void)showAuthenticationFallBackView:(NSString*)fallbackPage
{
_authenticationScrollView.hidden = YES;
_authFallbackContentView.hidden = NO;
// Add a cancel button in case of navigation controller use.
if (self.navigationController)
{
if (!cancelFallbackBarButton)
{
cancelFallbackBarButton = [[UIBarButtonItem alloc] initWithTitle:[VectorL10n loginLeaveFallback] style:UIBarButtonItemStylePlain target:self action:@selector(hideRegistrationFallbackView)];
}
// Add cancel button in right bar items
NSArray *rightBarButtonItems = self.navigationItem.rightBarButtonItems;
self.navigationItem.rightBarButtonItems = rightBarButtonItems ? [rightBarButtonItems arrayByAddingObject:cancelFallbackBarButton] : @[cancelFallbackBarButton];
}
if (self.softLogoutCredentials)
{
// Add device_id as query param of the fallback
NSURLComponents *components = [[NSURLComponents alloc] initWithString:fallbackPage];
NSMutableArray<NSURLQueryItem*> *queryItems = [components.queryItems mutableCopy];
if (!queryItems)
{
queryItems = [NSMutableArray array];
}
[queryItems addObject:[NSURLQueryItem queryItemWithName:@"device_id"
value:self.softLogoutCredentials.deviceId]];
components.queryItems = queryItems;
fallbackPage = components.URL.absoluteString;
}
[_authFallbackWebView openFallbackPage:fallbackPage success:^(MXLoginResponse *loginResponse) {
MXCredentials *credentials = [[MXCredentials alloc] initWithLoginResponse:loginResponse andDefaultCredentials:self->mxRestClient.credentials];
// TODO handle unrecognized certificate (if any) during registration through fallback webview.
[self onSuccessfulLogin:credentials];
}];
}
- (void)hideRegistrationFallbackView
{
if (cancelFallbackBarButton)
{
NSMutableArray *rightBarButtonItems = [NSMutableArray arrayWithArray: self.navigationItem.rightBarButtonItems];
[rightBarButtonItems removeObject:cancelFallbackBarButton];
self.navigationItem.rightBarButtonItems = rightBarButtonItems;
}
[_authFallbackWebView stopLoading];
_authenticationScrollView.hidden = NO;
_authFallbackContentView.hidden = YES;
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([@"viewHeightConstraint.constant" isEqualToString:keyPath])
{
// Refresh the height of the auth inputs view container.
CGFloat previousInputsContainerViewHeight = _authInputContainerViewHeightConstraint.constant;
_authInputContainerViewHeightConstraint.constant = _authInputsView.viewHeightConstraint.constant;
// Force to render the view
[self.view layoutIfNeeded];
// Refresh content view height by considering the updated height of inputs container
_contentViewHeightConstraint.constant += (_authInputContainerViewHeightConstraint.constant - previousInputsContainerViewHeight);
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
@end