Skip to content

Instantly share code, notes, and snippets.

@leeprobert
Created July 3, 2013 11:33
Show Gist options
  • Select an option

  • Save leeprobert/5917191 to your computer and use it in GitHub Desktop.

Select an option

Save leeprobert/5917191 to your computer and use it in GitHub Desktop.
AGNSession - Class for managing authentication and changes in permissions.
//
// AGNSession.h
// Agnitio iPlanner
//
// Created by Matt Gough on 04/02/2013.
// Copyright (c) 2013 Agnitio. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "AGNNetworkObject.h"
/*!
@brief The current authentication state of a session.
*/
typedef enum
{
//! The session has not been signed-in yet or was signed out
authenticationNone = 0,
/*! The session has authenticated locally against a previously
valid username and password, but due to a communications error has not been
able to authenticate against the live server. Failure to login due to invalid credentials
will not result in local authentication.
*/
authenticationLocal,
//! The session has authenticated against the live server
authenticationOnline
} AuthenticationState;
/*!
@brief AGNSession is a high level class that manages authentication with the M5 server.
@discussion It maintains state information about the logged in user and provides
easy mechanisms for getting relevant server-side URLs. It also sends notifications
about state changes to the authentication.
*/
@interface AGNSession : NSObject <AGNNetworkDataReceiver>
/*!
@brief The default AGNSession object to use.
@discussion Conceptually there is no requirement for there to be only one session
active at a time. i.e You could sign-in to two different accounts at once by creating two separate AGNSession objects.
But currently there is no need for this functionality. As such having a globally accessible
session makes life easier.<br>
NOTE - If you do ever allow multiple sessions at once, you will have to update this class to manage each session's
cookies independantly. Currently all session use the same Cookie storage and hence will clash with their cookies.
@result The default session object
*/
+ (AGNSession*)defaultSession;
/*!
@brief Sets the default session
@see +[AGNSession defaultSession]
@param defaultSession The session to use as the default session. Set to nil to clear the default session
*/
+ (void)setDefaultSession:(AGNSession*)defaultSession;
/*!
@brief The URL of the M5 API that will be used by new sessions
@discussion If you have a session, you should use its apiURL property directly to
get the URL it is using as it may be different to the one that new sessions will use.
@result The M5 API URL for new sessions
*/
+ (NSURL*)apiURLForNewSessions;
/*!
@brief Resets the M5 API URL used by new sessions
@discussion AGNSession uses NSUserDefaults as the store for its URL key. Call this method when
the defaults may have changed to resolve the new value. Any changes do NOT affect the API URL
used by existing session.
@result YES indicates that the URL actually changed.
*/
+ (BOOL)resetAPIURLForNewSessions;
/*!
@brief A dictionary giving info about the running client app
@discussion Several M5 APIs take a clientInfo dictionary as part of their request data. This method
returns such a dictionary. The dictionary will conform to the clientinfoschema JSON schema
@result The client info dictionary
*/
+ (NSMutableDictionary*)clientInfoDictionary;
/*!
@brief The designated initializer for AGNSession
@discussion Creates a new session. The apiURL becomes fixed at creation time and cannot be changed
afterwards. It is recommended that after finishing with a session that you release it. Do not try
to re-use a session that was previously created for the same user.
@param userName The username associated with this session. This cannot be changed afterwards.
@result The newly created Session.
*/
- (AGNSession*)initWithUserName:(NSString*)username;
//! Convenience for creating a session @see -[AGNSession initWithUserName:]
+ (AGNSession*)sessionWithUserName:(NSString*)username;
/*!
@brief Get the URL to use for making API requests to the server.
@discussion Constructs the correct API URL, taking into account the apiURL and version
of the API the client is using.
@param tag The AGNNetworkObjectTag for the request. You should NOT attempt to manually create requests for
kAGNNetworkObjectM5SignIn, kAGNNetworkObjectM5SignOut or kAGNNetworkObjectM5UserInfo. You should use
the signIn, signOut and updateUserInfo methods instead.
@param lastComponent - An optional component to append to the URL.
@result The URL to use with the request
@see -[AGNSession URLForRequestTag:]
*/
- (NSURL*)URLForRequestTag:(AGNNetworkObjectTag)tag optionalLastComponent:(NSString*)lastComponent;
/*!
@brief Get the URL to use for making API requests to the server.
@discussion Constructs the correct API URL, taking into account the apiURL and version
of the API the client is using.
@param tag The AGNNetworkObjectTag for the request. You should NOT attempt to manually create requests for
kAGNNetworkObjectM5SignIn, kAGNNetworkObjectM5SignOut or kAGNNetworkObjectM5UserInfo. You should use
the signIn, signOut and updateUserInfo methods instead.
@result The URL to use with the request
@see -[AGNSession URLForRequestTag:optionalLastComponent:]
*/
- (NSURL*)URLForRequestTag:(AGNNetworkObjectTag)tag;
/*!
@brief Attempt to sign in to the server
@discussion Sends off an asynchronous request to sign in to the server. You MUST have previously set the
password property beforehand. Once the request completes, a kAGNSessionDidSomethingInterestingNotification
will be posted.
*/
- (void)signIn;
/*!
@brief Attempt to sign out of the server
@discussion Cancels all previously enqueued requests to the server and sends off an asynchronous request to sign out. This request will complete even if you release the session before it completes to try to ensure the server receives the request. It is recommended that after you sign out that you do not attempt to re-use the same session. Before submitting the request this will lower the authenticationState to authenticationNone (if it is not already) and post a kAGNSessionDidSomethingInterestingNotification.
*/
- (void)signOut;
/*!
@brief Attempt to update the userInfo dictionary
@discussion Sends off an asynchronous request the server to get the user's account info. If there is already such a request in progress this call will do nothing. The session automatically calls this method every few minutes (when online) so there should not be a need to call it manually. Whenever the userInfo changes, the server will notify you via kAGNSessionDidSomethingInterestingNotification.
*/
- (void)updateUserInfo;
/*!
@brief Adds a network request to the session's request queue
@discussion Enqueues the request on the NSOperationQueue that the session maintains for requests. To avoid having multiple requests in-flight at the same time, the session maintains a concurrent queue with a set number of concurrent operations. As such, requests enqueued this way may not be sent immediately. This queue is automatically cleared at signOut.
@param object The network object to enqueue
@result Returns YES if the object was successfully enqueued. Attempting to enqueue whilst authenticationState
is not authenticationStateOnline will always fail.
*/
- (BOOL)enqueueNetworkObject:(AGNNetworkObject*)object;
/*!
@brief Returns an array of previously enqueued network objects
@discussion Use this to obtain an array of all the currently enqueued network objects that match
the specified tag and/or delegate
@param tag The AGNNetworkObjectTag of requests you want. Pass kAGNNetworkObjectMatchAnyTag to match against any tag
@param delegate The delegate associated with network object. Pass nil if you don't need to match a
specific delegate
@result The array of matching requests
*/
- (NSArray*)enqueuedNetworkObjectsWithTag:(AGNNetworkObjectTag)tag andDelegate:(id<AGNNetworkDataReceiver>)delegate;
/*!
@brief The URL of the M5 api used by this session.
@discussion This property is fixed when the session is created. You should not use it directly
to create URLs for network requests, but it is useful for getting other info such as the host name
and URL Scheme.
*/
@property (nonatomic, readonly) NSURL* apiURL;
//! The version of the M5 API being used. The current version is "1.0".
@property (nonatomic, readonly) NSString* apiVersion;
//! The username (e.g user@agnitio.com) of the account
@property (nonatomic, readonly) NSString* username;
//! The current authenticationState of the session. @see AuthenticationState
@property (nonatomic, readonly) AuthenticationState authenticationState;
//! The password to use when signing into the account. MUST be set before calling signin
@property (nonatomic, copy) NSString* password;
//! Whether the user has accepted the terms of the EULA
@property (nonatomic, assign) BOOL hasAcceptedTerms;
/*!
@brief User Info as provided by the server.
@discussion The most recent userInfo associated with the user as returned from the server.
This info is persistent and can be accessed without having to signIn. Will be nil if no info
has ever been received.
@result The contents of this dictionary conforms to the userinfoschema schema.
*/
@property (nonatomic, readonly) NSDictionary* userInfo;
// These are accessors into the user info dictionary - Preferred over accessing userInfo directly
//! The unique id given to the user by the server. NOT the same as username
@property (nonatomic, readonly) NSString* userId;
//! Whether the user is required to change their password
@property (nonatomic, readonly) BOOL enforcePasswordChange;
//! Whether the user is allowed to see duration of calls
@property (nonatomic, readonly) BOOL showCallDuration;
//! The Dynamic Attributes for the user. NOT CURRENTLY IMPLEMENTED
@property (nonatomic, readonly) NSDictionary* dynamicAttributes;
/* Local Folders
These methods return URLs for various locations in the device's file system where
info can be saved that is session and user related.
Where possible store everything related to Engager beneath one of these URLs.
To avoid mixing info from different host servers together, each host gets its own
sub-folder in which to store stuff. This is different to iPlanner 1.x which mixed
everything together.
HOWEVER - This is still not true for Content (Presentations/magazines etc), which currently still all get
clumped together in ~/Library/Caches/Presentations. There is no conceptual reason why
content can't also be moved into the host-specific domain, but there are practical reasons which
I was hoping to fix once the code was no longer tied to the iPlanner 1.x code-base. These include:
• The 'viewer' folder, which has to be at the same path as the root folder of presentations
to enable content to be able to access it consistently. If content were made host-specific
then each host would need its own copy of the viewer folder.
• In Caches folder content has a chance to be purged by the OS is free space is low. This
is both a blessing and a curse, but moving it out of content would stop thi happening.
One solution to the multiple viewer folder problem would be to still keep all of the content in
a single folder, but encode the name of each content's root folder to also include the host name.
E.g agnitio.com-12345, bayer.com-12345 etc. This way content with the same id won't clash
across differnt hosts, but they can all share the same viewer folder. The ideal location for this
would be rootFileURLForAllHosts/Content/. Left as an exercise for the reader.
As things currently stand, here is an example hierarchy for a user 'user@example.com' on host 'agnitio.com' and their equivalent method:
--Library
----Caches
------Presentations
--------12345
--------viewer
------Snapshots
----Hosts +[AGNSession rootFileURLForAllHosts]
------agnitio.com +[AGNSession rootFileURLForHostURL:]
--------Users +[AGNSession rootFileURLForAllUsersOfHostURL:]
----------user@example.com +[AGNSession rootFileURLForUser:ofHostURL:] or -[AGNSession rootFileURLForUser]
------------<persistent data goes in here> e.g calls.plist, contacts.db, monitoring info etc
------------SessionPrivate -[AGNSession fileURLForSessionPrivateDirectory]
Note that 'FileURL' does not imply a file or a directory, just that the URL returned is file based. It just so happens
that currently they are all directories.
*/
/*!
The folder beneath which all persistent data for Engager SHOULD be stored.
Should not be used directly, use a more specific method instead.
@see -[AGNSession rootFileURLForUser];
*/
+ (NSURL*)rootFileURLForAllHosts;
/*!
The folder beneath which all persistent data for a specific host SHOULD be stored.
Should not be used directly unless you implement the above suggestion about where to
store Content.
@see -[AGNSession rootFileURLForUser];
*/
+ (NSURL*)rootFileURLForHostURL:(NSURL*)hostURL;
/*!
The folder beneath which all persistent user-related data for a specific host SHOULD be stored.
Should not be used directly, use a more specific method instead.
@see -[AGNSession rootFileURLForUser];
*/
+ (NSURL*)rootFileURLForAllUsersOfHostURL:(NSURL*)hostURL;
/*!
The folder beneath which all persistent user-related data for a specific user on a specific host SHOULD be stored.
@see -[AGNSession rootFileURLForUser];
*/
+ (NSURL*)rootFileURLForUser:(NSString*)username ofHostURL:(NSURL*)hostURL;
//! The folder beneath which all persistent user-related data for the current session SHOULD be stored.
@property (nonatomic, readonly) NSURL* rootFileURLForUser;
//! The folder beneath which AGNSession saves its own private data. DO NOT USE except to find out its location. Store nothing in here
@property (nonatomic, readonly) NSURL* fileURLForSessionPrivateDirectory;
@end
// Notifications posted by AGNSession
/*!
@brief Notification posted by a session to signal state changes.
@discussion To avoid having multiple notifications for each type of state change, and to be able to coalesce multiple
concurrent state changes into one notification we only have this one notification.
@param notification.object The session that posted the notification
@param notification.userInfo The user info dictionary contains the following fields:
@param kAGNSessionInterestingFlagsKey An NSNumber containing a mask of the events that caused the notification.
@param NSUnderlyingError If there was an error associated with the event this will contain the underlying NSError.
@see AGNSessionInterestingFlags
*/
extern NSString* const kAGNSessionDidSomethingInterestingNotification;
/*!
@brief Flags associated with kAGNSessionDidSomethingInterestingNotification
*/
enum AGNSessionInterestingFlags
{
//! Indicates that the session has finshed attempting to sign in (whether successful or not)
sessionSignInFinished = 1 << 0,
//! Indicates that the session's authenticationState has changed
sessionAuthenticationStateChanged = 1 << 1,
//! Indicates that the session's userInfo has changed
sessionUserInfoChanged = 1 << 2,
//! Indicates that the user has accepted the terms of the EULA
sessionAcceptedTermsChanged = 1 << 3
};
typedef NSUInteger AGNSessionInterestingFlags;
extern NSString* const kAGNSessionInterestingFlagsKey; // an NSNumber of AGNSessionInterestingFlags
//
// AGNSession.m
// Agnitio iPlanner
//
// Created by Matt Gough on 04/02/2013.
// Copyright (c) 2013 Agnitio. All rights reserved.
//
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
#import "AGNSession.h"
#import "AGNFacade2Constants.h"
#import "UIDevice+DeviceID.h"
#import "AGNJSONValidator.h"
#import "PDKeychainBindingsController.h"
#import "AGNDataWriting.h"
NSString* const kAGNSessionDidSomethingInterestingNotification = @"AGNSessionDidSomethingInterestingNotification";
NSString* const kAGNSessionInterestingFlagsKey = @"AGNSessionInterestingFlags";
NSString* const kAGNSessionUserInfoKey = @"userInfo"; // NSDictionary
NSString* const kAGNSessionAcceptedTermsKey = @"acceptedTerms"; // NSNumber (bool)
@interface AGNUserInfoNetworkObject : AGNNetworkObject
@property (nonatomic, assign) BOOL isAutomaticRequest;
@end
@implementation AGNUserInfoNetworkObject
@end
@interface AGNSession () {
NSTimer* _userInfoTimer;
NSMutableDictionary* _sessionInfo;
NSURL* _rootFileURLForUser;
NSOperationQueue* _requestQueue;
AGNNetworkObject* _signOutRequest;
}
@end
@implementation AGNSession
static AGNSession* sDefaultSession = nil;
static NSURL* sAPIURL = nil;
+ (AGNSession*)defaultSession
{
@synchronized(self)
{
return sDefaultSession;
}
}
+ (void)setDefaultSession:(AGNSession*)currentSession
{
@synchronized(self)
{
if (currentSession != sDefaultSession)
{
sDefaultSession = currentSession;
}
}
}
+ (NSURL*)apiURLForNewSessions
{
@synchronized(self)
{
#define kDefaultURLString @"https://www.agnitio.com" // TODO - what should it really be
if (!sAPIURL)
{
[[NSUserDefaults standardUserDefaults] synchronize];
NSString *tFacadeURL = [[NSUserDefaults standardUserDefaults] valueForKey:@"ifacade_url"];
if (![tFacadeURL length])
{
// Get it back into defaults
tFacadeURL = kDefaultURLString;
[[NSUserDefaults standardUserDefaults] setValue:tFacadeURL forKey:@"ifacade_url"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
else
{
sAPIURL = [[NSURL alloc] initWithString:tFacadeURL];
if (!sAPIURL)
AGNLogError(@"Invalid API URL string:%@", tFacadeURL);
}
if (!sAPIURL)
sAPIURL = [[NSURL alloc] initWithString:kDefaultURLString];
}
}
return sAPIURL;
}
+ (BOOL)resetAPIURLForNewSessions
{
BOOL changed = NO;
@synchronized(self)
{
NSURL* oldURL = [self apiURLForNewSessions];
sAPIURL = nil;
NSURL* newURL = [self apiURLForNewSessions];
changed = ![oldURL isEqual:newURL];
}
return changed;
}
+ (AGNSession*)sessionWithUserName:(NSString*)username
{
return [[self alloc] initWithUserName:username];
}
-(AGNSession*)initWithUserName:(NSString*)username
{
self = [super init];
if (self)
{
_apiURL = [AGNSession apiURLForNewSessions];
_requestQueue = [[NSOperationQueue alloc] init];
[_requestQueue setMaxConcurrentOperationCount:5];
[_requestQueue setName:[NSString stringWithFormat:@"AGNSession request queue(%@)", username]];
_username = [username copy];
_rootFileURLForUser = [[self class] rootFileURLForUser:_username ofHostURL:_apiURL];
_password = [AGNSession savedPasswordForUsername:_username];
_sessionInfo = [NSMutableDictionary dictionaryWithContentsOfURL:[self fileURLForSessionPrivateData]] ?: [NSMutableDictionary dictionary];
}
return self;
}
- (void)dealloc
{
[self stopUserInfoTimer];
[self detachSignOutRequest];
// clear out any delegates that are this session. Otherwise we will get a finishedWithError call
// after we are destroyed.
NSArray* mine = [_requestQueue operationsWithNetworkObjectTag:kAGNNetworkObjectMatchAnyTag andDelegate:self];
for (AGNNetworkObject* object in mine)
object.delegate = nil;
[self cancelCurrentRequests];
}
- (void)detachSignOutRequest
{
// Detach the signOut request from the session.
// We don't cancel the request as we have to let it complete
if (_signOutRequest)
{
_signOutRequest.delegate = nil;
_signOutRequest = nil;
}
}
- (NSString*)apiVersion
{
return @"1.0";
}
- (NSString*)userId
{
return self.userInfo[kUserIdKey];
}
- (BOOL)enforcePasswordChange
{
return [self.userInfo[kUserSettingsKey][kEnforcePasswordChangeKey] boolValue];
}
- (BOOL)showCallDuration
{
return [self.userInfo[kUserSettingsKey][kShowCallDurationKey] boolValue];
}
- (BOOL)hasAcceptedTerms
{
return [_sessionInfo[kAGNSessionAcceptedTermsKey] boolValue];
}
- (void)setHasAcceptedTerms:(BOOL)hasAcceptedTerms
{
BOOL oldValue = self.hasAcceptedTerms;
if (hasAcceptedTerms != oldValue)
{
_sessionInfo[kAGNSessionAcceptedTermsKey] = @(hasAcceptedTerms);
[self saveSessionInfo];
[self postDidSomethingInterestingNotificationIfNeededForFlags:sessionAcceptedTermsChanged error:nil];
}
}
- (NSURL*)URLForRequestTag:(AGNNetworkObjectTag)tag optionalLastComponent:(NSString*)lastComponent
{
NSURL* versionURL = [_apiURL URLByAppendingPathComponent:self.apiVersion];
NSURL* result = nil;
switch (tag)
{
case kAGNNetworkObjectM5SignIn:
result = [versionURL URLByAppendingPathComponent:@"user/login"];
break;
case kAGNNetworkObjectM5SignOut:
result = [versionURL URLByAppendingPathComponent:@"user/logout"];
break;
case kAGNNetworkObjectM5UserInfo:
result = [versionURL URLByAppendingPathComponent:@"user"];
break;
case kAGNNetworkObjectM5SupportFilesContentList:
result = [versionURL URLByAppendingPathComponent:@"content/iOSSupportFiles"];
break;
case kAGNNetworkObjectM5DocumentList:
result = [versionURL URLByAppendingPathComponent:@"content/info"];
break;
case kAGNNetworkObjectM5DocumentContentList:
result = [versionURL URLByAppendingPathComponent:@"content"];
break;
case kAGNNetworkObjectM5ContactsInfo:
result = [versionURL URLByAppendingPathComponent:@"contact/info"];
break;
case kAGNNetworkObjectM5Contacts:
result = [versionURL URLByAppendingPathComponent:@"contact"];
break;
case kAGNNetworkObjectM5Calls:
result = [versionURL URLByAppendingPathComponent:@"call"];
break;
case kAGNNetworkObjectM5UploadMonitoringData:
result = [versionURL URLByAppendingPathComponent:@"analytics"];
break;
default:
break;
}
if ([lastComponent length])
result = [result URLByAppendingPathComponent:lastComponent];
NSAssert(result, @"Can't create URL for iFacade tag %d", tag);
return result;
}
- (NSURL*)URLForRequestTag:(AGNNetworkObjectTag)tag
{
return [self URLForRequestTag:tag optionalLastComponent:nil];
}
- (void)cancelCurrentRequests
{
[_requestQueue cancelAllOperations];
if (_signOutRequest)
{
[_signOutRequest cancel];
[self detachSignOutRequest];
}
}
- (BOOL)enqueueNetworkObject:(AGNNetworkObject*)object
{
if (!object)
return NO;
if (self.authenticationState != authenticationOnline)
return NO; // Can't add requests when not signed in
[_requestQueue addOperation:object];
return YES;
}
- (NSArray*)enqueuedNetworkObjectsWithTag:(AGNNetworkObjectTag)tag andDelegate:(id<AGNNetworkDataReceiver>)delegate
{
return [_requestQueue operationsWithNetworkObjectTag:tag andDelegate:delegate];
}
+ (NSMutableDictionary*)clientInfoDictionary
{
NSMutableDictionary* result = [NSMutableDictionary dictionary];
NSBundle* mainBundle = [NSBundle mainBundle];
UIDevice* currentDevice = [UIDevice currentDevice];
NSString* appName = [mainBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
NSString* appVersion = [mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
NSString* bundleVersion = [mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"];
NSString* iosVersion = [currentDevice systemVersion];
result[kClientInfoPlatformIdKey] = [currentDevice platform];
result[kClientInfoOSKey] = @"iOS";
result[kClientInfoOSVersionKey] = iosVersion;
result[kClientInfoClientFamilyKey] = kClientFamily;
result[kClientInfoClientIdKey] = [mainBundle bundleIdentifier];
result[kClientInfoClientNameKey] = appName;
result[kClientInfoClientVersionKey] = appVersion;
result[kClientInfoClientBuildVersionKey]= bundleVersion;
result[kClientInfoTotalRAMKey] = [currentDevice totalRAM];
result[kClientInfoFreeRAMKey] = [currentDevice freeRAM];
result[kClientInfoTotalDiskSpaceKey] = [currentDevice totalDiskSpace];
result[kClientInfoFreeDiskSpaceKey] = [currentDevice freeDiskSpace];
result[kClientInfoPreferredLanguageKey] = [NSLocale preferredLanguages][0];
result[kClientInfoClientLanguageKey] = [mainBundle preferredLocalizations][0];
return result;
}
- (void)signIn
{
NSAssert([_username length], @"No username specified");
NSAssert([_password length], @"No password specified");
[self cancelCurrentRequests];
NSHTTPCookieStorage* cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieStorage cookies])
[cookieStorage deleteCookie:cookie];
NSURL* url = [self URLForRequestTag:kAGNNetworkObjectM5SignIn];
AGNNetworkObject* request = [AGNNetworkObject networkObjectWithURL:url delegate:self];
request.tag = kAGNNetworkObjectM5SignIn;
NSDictionary* body = @{
kSignInUsernameKey:self.username,
kSignInPasswordKey:self.password,
kClientInfoKey:[AGNSession clientInfoDictionary]
};
request.postBody = body;
request.postBodyJSONValidationSchemaURL = [AGNJSONValidator URLForSchemaNamed:kSignInRequestSchemaName];
request.queuePriority = NSOperationQueuePriorityHigh;
request.JSONValidationSchemaURL = [AGNJSONValidator URLForSchemaNamed:kSignInSchemaName];
[_requestQueue addOperation:request];
}
- (void)updateUserInfoForAutomaticRequest:(BOOL)isAutomaticRequest
{
if (_authenticationState != authenticationOnline)
return; // Nothing to do
if ([[_requestQueue operationsWithNetworkObjectTag:kAGNNetworkObjectM5UserInfo andDelegate:self] count])
return; // Already doing this. Lets not add a duplicate request
NSURL* url = [self URLForRequestTag:kAGNNetworkObjectM5UserInfo];
AGNUserInfoNetworkObject* userInfoRequest = [AGNUserInfoNetworkObject networkObjectWithURL:url delegate:self];
userInfoRequest.tag = kAGNNetworkObjectM5UserInfo;
userInfoRequest.queuePriority = NSOperationQueuePriorityHigh;
userInfoRequest.isAutomaticRequest = isAutomaticRequest;
userInfoRequest.JSONValidationSchemaURL = [AGNJSONValidator URLForSchemaNamed:kUserInfoSchemaName];
[_requestQueue addOperation:userInfoRequest];
}
- (void)updateUserInfo
{
[self updateUserInfoForAutomaticRequest:NO];
}
- (void)signOut
{
[self cancelCurrentRequests];
/* Note. We don't enqueue the signOut request with the other requests as otherwise
if the session gets torn down as a result of the updated authentication state we may
not get around to actually completing the request.
We do our best to get the request to the server so that it can clear up whatever state
it maintains about the session
*/
_signOutRequest = [[AGNNetworkObject alloc] initWithURL:[self URLForRequestTag:kAGNNetworkObjectM5SignOut]
delegate:self];
_signOutRequest.postBody = @"";
_signOutRequest.tag = kAGNNetworkObjectM5SignOut;
_signOutRequest.JSONValidationSchemaURL = [AGNJSONValidator URLForSchemaNamed:kGenericSuccessSchemaName];
[_signOutRequest start];
AGNSessionInterestingFlags flags = [self updateAuthenticationState:authenticationNone];
[self postDidSomethingInterestingNotificationIfNeededForFlags:flags error:nil];
}
- (void)userInfoTimer:(NSTimer*)timer
{
[self stopUserInfoTimer]; // Throw away current timer
[self updateUserInfoForAutomaticRequest:YES];
}
- (void)startUserInfoTimer
{
#if DEBUG
NSTimeInterval interval = 60;
#else
NSTimeInterval interval = 300;
#endif
[self stopUserInfoTimer];
_userInfoTimer = [NSTimer scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(userInfoTimer:)
userInfo:nil
repeats:NO];
}
- (void)stopUserInfoTimer
{
[_userInfoTimer invalidate];
_userInfoTimer = nil;
}
- (AGNSessionInterestingFlags)updateAuthenticationState:(AuthenticationState)authenticationState
{
AGNSessionInterestingFlags result = 0;
if (_authenticationState != authenticationState)
{
result = sessionAuthenticationStateChanged;
_authenticationState = authenticationState;
if (_authenticationState == authenticationOnline)
[self startUserInfoTimer];
else
[self stopUserInfoTimer];
}
return result;
}
- (NSDictionary*)userInfo
{
NSDictionary* result = [_sessionInfo[kAGNSessionUserInfoKey] copy];
return result;
}
- (void)saveSessionInfo
{
@synchronized(_sessionInfo)
{
[_sessionInfo writeToFileURL:self.fileURLForSessionPrivateData
options:NSDataWritingAtomic | [Utils defaultFileProtection]
withIntermediateDirectories:YES
error:nil];
}
}
- (AGNSessionInterestingFlags)updateUserInfo:(NSDictionary *)userInfo
{
userInfo = userInfo ?: @{};
AGNSessionInterestingFlags result = 0;
@synchronized(_sessionInfo)
{
NSDictionary* currentUserInfo = self.userInfo;
if (![currentUserInfo isEqualToDictionary:userInfo])
{
result = sessionUserInfoChanged;
_sessionInfo[kAGNSessionUserInfoKey] = [userInfo copy];
[self saveSessionInfo];
}
}
return result;
}
- (void)postDidSomethingInterestingNotificationIfNeededForFlags:(AGNSessionInterestingFlags)flags error:(NSError*)error
{
if (flags)
{
NSMutableDictionary* notificationInfo = [NSMutableDictionary dictionaryWithCapacity:2];
notificationInfo[kAGNSessionInterestingFlagsKey] = @(flags);
if (error)
notificationInfo[NSUnderlyingErrorKey] = error;
[[NSNotificationCenter defaultCenter] postNotificationName:kAGNSessionDidSomethingInterestingNotification
object:self
userInfo:notificationInfo];
}
}
#pragma mark AGNNetworkDataReceiver
- (void) requestFinishedWithObject:(AGNNetworkObject *)object
{
NSError* error = object.error;
AGNSessionInterestingFlags flags = 0;
switch (object.tag)
{
case kAGNNetworkObjectM5SignIn:
{
flags = sessionSignInFinished;
AuthenticationState newAuthState = authenticationNone;
if (!error)
{
NSDictionary* result = object.validatedJSONValue;
NSDictionary* userInfo = result[kUserInfoKey];
if (userInfo)
flags |= [self updateUserInfo:userInfo];
[AGNSession savePassword:self.password forUsername:self.username];
newAuthState = authenticationOnline;
}
else
{
AGNLogError(@"Failed to sign in: %@", error);
NSString* errorDomain = [error domain];
NSString* savedPassword = [AGNSession savedPasswordForUsername:self.username];
BOOL isSavedPassword = [savedPassword length] && [self.password isEqualToString:savedPassword];
if ([errorDomain isEqualToString:NSURLErrorDomain])
{
// If the password they entered is the same as the saved one then we can authorize them locally
if (isSavedPassword)
newAuthState = authenticationLocal;
}
else if ([errorDomain isEqualToString:AGNHTTPStatusErrorDomain])
{
if (isSavedPassword)
{
NSInteger statusCode = [error code];
if (statusCode == 401)
{
// Invalid credentials (either user name or password)
// Remove saved password for this user as we know its no longer valid
// and so we can't allow them to authorize locally anymore.
[AGNSession savePassword:nil forUsername:self.username];
}
else if (statusCode == 404) // Server misbehaving??
newAuthState = authenticationLocal;
}
}
}
flags |= [self updateAuthenticationState:newAuthState];
break;
}
case kAGNNetworkObjectM5SignOut:
{
break;
}
case kAGNNetworkObjectM5UserInfo:
{
if (!error)
{
NSDictionary* userInfo = object.validatedJSONValue;
if (userInfo)
flags |= [self updateUserInfo:userInfo];
}
if ([object isKindOfClass:[AGNUserInfoNetworkObject class]] && ((AGNUserInfoNetworkObject*)object).isAutomaticRequest)
{
[self startUserInfoTimer];
}
break;
}
default:
break;
}
[self postDidSomethingInterestingNotificationIfNeededForFlags:flags error:error];
if (object == _signOutRequest)
[self detachSignOutRequest];
}
#pragma mark - Files and Folders
+ (NSURL*)rootFileURLForAllHosts
{
NSString* path = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES)[0];
path = [path stringByAppendingPathComponent:@"Hosts"];
NSURL* result = [NSURL fileURLWithPath:path isDirectory:YES];
return result;
}
+ (NSURL*)rootFileURLForHostURL:(NSURL*)hostURL
{
NSString* hostName = [[hostURL host] lowercaseString];
NSURL* result = [[self rootFileURLForAllHosts] URLByAppendingPathComponent:hostName isDirectory:YES];
return result;
}
+ (NSURL*)rootFileURLForAllUsersOfHostURL:(NSURL*)hostURL
{
NSURL* result = [[self rootFileURLForHostURL:hostURL] URLByAppendingPathComponent:@"Users" isDirectory:YES];
return result;
}
+ (NSURL*)rootFileURLForUser:(NSString*)username ofHostURL:(NSURL*)hostURL
{
NSURL* result = [[self rootFileURLForAllUsersOfHostURL:hostURL] URLByAppendingPathComponent:[self usernameKeyForUsername:username] isDirectory:YES];
return result;
}
+ (NSString*)usernameKeyForUsername:(NSString*)username
{
if (![username length])
{
/* Whenever there is no user name we still need to define a sensible
directory for this to avoid having to worry about exceptions if anything
tries to read/write to this directory. I chose "(null)" as that is what the old stringWithFormat
version of the path appending code would end up with.
*/
return @"(null)";
}
NSMutableString* result = [[username lowercaseString] mutableCopy];
// %-encode away any '/'s in the key since that doesn't play well with the file system
// First we have to %-encode the existing '%' characters
[result replaceOccurrencesOfString:@"%" withString:@"%25" options:0 range:NSMakeRange(0, [result length])];
[result replaceOccurrencesOfString:@"/" withString:@"%2F" options:0 range:NSMakeRange(0, [result length])];
return result;
}
- (NSURL*)fileURLForSessionPrivateDirectory
{
NSURL* result = [self.rootFileURLForUser URLByAppendingPathComponent:@"SessionPrivate" isDirectory:YES];
return result;
}
- (NSURL*)fileURLForSessionPrivateData
{
NSURL* result = [[self fileURLForSessionPrivateDirectory] URLByAppendingPathComponent:@"Settings.plist" isDirectory:NO];
return result;
}
#pragma mark - Keychain
+ (NSString*)savedPasswordForUsername:(NSString*)username
{
return [[PDKeychainBindingsController sharedKeychainBindingsController] stringForKey:[username lowercaseString]];
}
+ (void)savePassword:(NSString*)password forUsername:(NSString*)username
{
[[PDKeychainBindingsController sharedKeychainBindingsController] storeString:password forKey:[username lowercaseString]];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment