2
0
MasterPassword/External/google-plus-ios-sdk/OpenSource/GTMOAuth2ViewControllerTouch.m

1048 lines
36 KiB
Mathematica
Raw Normal View History

/* Copyright (c) 2011 Google Inc.
*
* 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.
*/
//
// GTMOAuth2ViewControllerTouch.m
//
#import <Foundation/Foundation.h>
#import <Security/Security.h>
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
#if TARGET_OS_IPHONE
#define GTMOAUTH2VIEWCONTROLLERTOUCH_DEFINE_GLOBALS 1
#import "GTMOAuth2ViewControllerTouch.h"
#import "GTMOAuth2SignIn.h"
#import "GTMOAuth2Authentication.h"
static NSString * const kGTMOAuth2AccountName = @"OAuth";
static GTMOAuth2Keychain* sDefaultKeychain = nil;
@interface GTMOAuth2ViewControllerTouch()
@property (nonatomic, copy) NSURLRequest *request;
- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request;
- (void)signIn:(GTMOAuth2SignIn *)signIn
finishedWithAuth:(GTMOAuth2Authentication *)auth
error:(NSError *)error;
- (BOOL)isNavigationBarTranslucent;
- (void)moveWebViewFromUnderNavigationBar;
- (void)popView;
- (void)clearBrowserCookies;
@end
@implementation GTMOAuth2ViewControllerTouch
// IBOutlets
@synthesize request = request_,
backButton = backButton_,
forwardButton = forwardButton_,
navButtonsView = navButtonsView_,
rightBarButtonItem = rightBarButtonItem_,
webView = webView_,
initialActivityIndicator = initialActivityIndicator_;
@synthesize keychainItemName = keychainItemName_,
keychainItemAccessibility = keychainItemAccessibility_,
initialHTMLString = initialHTMLString_,
browserCookiesURL = browserCookiesURL_,
signIn = signIn_,
userData = userData_,
properties = properties_;
#if NS_BLOCKS_AVAILABLE
@synthesize popViewBlock = popViewBlock_;
#endif
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (id)controllerWithScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret
keychainItemName:(NSString *)keychainItemName
delegate:(id)delegate
finishedSelector:(SEL)finishedSelector {
return [[[self alloc] initWithScope:scope
clientID:clientID
clientSecret:clientSecret
keychainItemName:keychainItemName
delegate:delegate
finishedSelector:finishedSelector] autorelease];
}
- (id)initWithScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret
keychainItemName:(NSString *)keychainItemName
delegate:(id)delegate
finishedSelector:(SEL)finishedSelector {
// convenient entry point for Google authentication
Class signInClass = [[self class] signInClass];
GTMOAuth2Authentication *auth;
auth = [signInClass standardGoogleAuthenticationForScope:scope
clientID:clientID
clientSecret:clientSecret];
NSURL *authorizationURL = [signInClass googleAuthorizationURL];
return [self initWithAuthentication:auth
authorizationURL:authorizationURL
keychainItemName:keychainItemName
delegate:delegate
finishedSelector:finishedSelector];
}
#if NS_BLOCKS_AVAILABLE
+ (id)controllerWithScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret
keychainItemName:(NSString *)keychainItemName
2013-08-11 04:08:25 +00:00
completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler {
return [[[self alloc] initWithScope:scope
clientID:clientID
clientSecret:clientSecret
keychainItemName:keychainItemName
completionHandler:handler] autorelease];
}
- (id)initWithScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret
keychainItemName:(NSString *)keychainItemName
2013-08-11 04:08:25 +00:00
completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler {
// convenient entry point for Google authentication
Class signInClass = [[self class] signInClass];
GTMOAuth2Authentication *auth;
auth = [signInClass standardGoogleAuthenticationForScope:scope
clientID:clientID
clientSecret:clientSecret];
NSURL *authorizationURL = [signInClass googleAuthorizationURL];
self = [self initWithAuthentication:auth
authorizationURL:authorizationURL
keychainItemName:keychainItemName
delegate:nil
finishedSelector:NULL];
if (self) {
completionBlock_ = [handler copy];
}
return self;
}
#endif // NS_BLOCKS_AVAILABLE
#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
keychainItemName:(NSString *)keychainItemName
delegate:(id)delegate
finishedSelector:(SEL)finishedSelector {
return [[[self alloc] initWithAuthentication:auth
authorizationURL:authorizationURL
keychainItemName:keychainItemName
delegate:delegate
finishedSelector:finishedSelector] autorelease];
}
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
keychainItemName:(NSString *)keychainItemName
delegate:(id)delegate
finishedSelector:(SEL)finishedSelector {
NSString *nibName = [[self class] authNibName];
NSBundle *nibBundle = [[self class] authNibBundle];
self = [super initWithNibName:nibName bundle:nibBundle];
if (self != nil) {
delegate_ = [delegate retain];
finishedSelector_ = finishedSelector;
Class signInClass = [[self class] signInClass];
// use the supplied auth and OAuth endpoint URLs
signIn_ = [[signInClass alloc] initWithAuthentication:auth
authorizationURL:authorizationURL
delegate:self
webRequestSelector:@selector(signIn:displayRequest:)
finishedSelector:@selector(signIn:finishedWithAuth:error:)];
// if the user is signing in to a Google service, we'll delete the
// Google authentication browser cookies upon completion
//
// for other service domains, or to disable clearing of the cookies,
// set the browserCookiesURL property explicitly
NSString *authorizationHost = [signIn_.authorizationURL host];
if ([authorizationHost hasSuffix:@".google.com"]) {
NSString *urlStr = [NSString stringWithFormat:@"https://%@/",
authorizationHost];
NSURL *cookiesURL = [NSURL URLWithString:urlStr];
[self setBrowserCookiesURL:cookiesURL];
}
[self setKeychainItemName:keychainItemName];
}
return self;
}
#if NS_BLOCKS_AVAILABLE
+ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
keychainItemName:(NSString *)keychainItemName
2013-08-11 04:08:25 +00:00
completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler {
return [[[self alloc] initWithAuthentication:auth
authorizationURL:authorizationURL
keychainItemName:keychainItemName
completionHandler:handler] autorelease];
}
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
keychainItemName:(NSString *)keychainItemName
2013-08-11 04:08:25 +00:00
completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler {
// fall back to the non-blocks init
self = [self initWithAuthentication:auth
authorizationURL:authorizationURL
keychainItemName:keychainItemName
delegate:nil
finishedSelector:NULL];
if (self) {
completionBlock_ = [handler copy];
}
return self;
}
#endif
- (void)dealloc {
[webView_ setDelegate:nil];
[backButton_ release];
[forwardButton_ release];
[initialActivityIndicator_ release];
[navButtonsView_ release];
[rightBarButtonItem_ release];
[webView_ release];
[signIn_ release];
[request_ release];
[delegate_ release];
#if NS_BLOCKS_AVAILABLE
[completionBlock_ release];
[popViewBlock_ release];
#endif
[keychainItemName_ release];
[initialHTMLString_ release];
[browserCookiesURL_ release];
[userData_ release];
[properties_ release];
[super dealloc];
}
+ (NSString *)authNibName {
// subclasses may override this to specify a custom nib name
return @"GTMOAuth2ViewTouch";
}
+ (NSBundle *)authNibBundle {
// subclasses may override this to specify a custom nib bundle
return nil;
}
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret {
Class signInClass = [self signInClass];
NSURL *tokenURL = [signInClass googleTokenURL];
NSString *redirectURI = [signInClass nativeClientRedirectURI];
GTMOAuth2Authentication *auth;
auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
tokenURL:tokenURL
redirectURI:redirectURI
clientID:clientID
clientSecret:clientSecret];
[[self class] authorizeFromKeychainForName:keychainItemName
authentication:auth];
return auth;
}
#endif
+ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName
authentication:(GTMOAuth2Authentication *)newAuth {
newAuth.accessToken = nil;
BOOL didGetTokens = NO;
GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain];
NSString *password = [keychain passwordForService:keychainItemName
account:kGTMOAuth2AccountName
error:nil];
if (password != nil) {
[newAuth setKeysForResponseString:password];
didGetTokens = YES;
}
return didGetTokens;
}
+ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName {
GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain];
return [keychain removePasswordForService:keychainItemName
account:kGTMOAuth2AccountName
error:nil];
}
+ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName
authentication:(GTMOAuth2Authentication *)auth {
return [self saveParamsToKeychainForName:keychainItemName
accessibility:NULL
authentication:auth];
}
+ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName
accessibility:(CFTypeRef)accessibility
authentication:(GTMOAuth2Authentication *)auth {
[self removeAuthFromKeychainForName:keychainItemName];
// don't save unless we have a token that can really authorize requests
if (![auth canAuthorize]) return NO;
if (accessibility == NULL
&& &kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly != NULL) {
accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
}
// make a response string containing the values we want to save
NSString *password = [auth persistenceResponseString];
GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain];
return [keychain setPassword:password
forService:keychainItemName
accessibility:accessibility
account:kGTMOAuth2AccountName
error:nil];
}
- (void)loadView {
NSString *nibPath = nil;
NSBundle *nibBundle = [self nibBundle];
if (nibBundle == nil) {
nibBundle = [NSBundle mainBundle];
}
NSString *nibName = self.nibName;
if (nibName != nil) {
nibPath = [nibBundle pathForResource:nibName ofType:@"nib"];
}
if (nibPath != nil && [[NSFileManager defaultManager] fileExistsAtPath:nibPath]) {
[super loadView];
} else {
// One of the requirements of loadView is that a valid view object is set to
// self.view upon completion. Otherwise, subclasses that attempt to
// access self.view after calling [super loadView] will enter an infinite
// loop due to the fact that UIViewController's -view accessor calls
// loadView when self.view is nil.
self.view = [[[UIView alloc] init] autorelease];
#if DEBUG
NSLog(@"missing %@.nib", nibName);
#endif
}
}
- (void)viewDidLoad {
2013-08-11 04:08:25 +00:00
[self setUpNavigation];
}
- (void)setUpNavigation {
rightBarButtonItem_.customView = navButtonsView_;
self.navigationItem.rightBarButtonItem = rightBarButtonItem_;
}
- (void)popView {
#if NS_BLOCKS_AVAILABLE
void (^popViewBlock)() = self.popViewBlock;
#else
id popViewBlock = nil;
#endif
if (popViewBlock || self.navigationController.topViewController == self) {
if (!self.view.hidden) {
// Set the flag to our viewWillDisappear method so it knows
// this is a disappearance initiated by the sign-in object,
// not the user cancelling via the navigation controller
didDismissSelf_ = YES;
if (popViewBlock) {
#if NS_BLOCKS_AVAILABLE
popViewBlock();
self.popViewBlock = nil;
#endif
} else {
[self.navigationController popViewControllerAnimated:YES];
}
self.view.hidden = YES;
}
}
}
- (void)notifyWithName:(NSString *)name
webView:(UIWebView *)webView
kind:(NSString *)kind {
BOOL isStarting = [name isEqual:kGTMOAuth2WebViewStartedLoading];
if (hasNotifiedWebViewStartedLoading_ == isStarting) {
// Duplicate notification
//
// UIWebView's delegate methods are so unbalanced that there's little
// point trying to keep a count, as it could easily end up stuck greater
// than zero.
//
// We don't really have a way to track the starts and stops of
// subframe loads, too, as the webView in the notification is always
// for the topmost request.
return;
}
hasNotifiedWebViewStartedLoading_ = isStarting;
// Notification for webview load starting and stopping
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
webView, kGTMOAuth2WebViewKey,
kind, kGTMOAuth2WebViewStopKindKey, // kind may be nil
nil];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:name
object:self
userInfo:dict];
}
- (void)cancelSigningIn {
// The application has explicitly asked us to cancel signing in
// (so no further callback is required)
hasCalledFinished_ = YES;
[delegate_ autorelease];
delegate_ = nil;
#if NS_BLOCKS_AVAILABLE
[completionBlock_ autorelease];
completionBlock_ = nil;
#endif
// The sign-in object's cancel method will close the window
[signIn_ cancelSigningIn];
hasDoneFinalRedirect_ = YES;
}
static Class gSignInClass = Nil;
+ (Class)signInClass {
if (gSignInClass == Nil) {
gSignInClass = [GTMOAuth2SignIn class];
}
return gSignInClass;
}
+ (void)setSignInClass:(Class)theClass {
gSignInClass = theClass;
}
#pragma mark Token Revocation
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
[[self signInClass] revokeTokenForGoogleAuthentication:auth];
}
#endif
#pragma mark Browser Cookies
- (GTMOAuth2Authentication *)authentication {
return self.signIn.authentication;
}
- (void)clearBrowserCookies {
// if browserCookiesURL is non-nil, then get cookies for that URL
// and delete them from the common application cookie storage
NSURL *cookiesURL = [self browserCookiesURL];
if (cookiesURL) {
NSHTTPCookieStorage *cookieStorage;
cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSArray *cookies = [cookieStorage cookiesForURL:cookiesURL];
for (NSHTTPCookie *cookie in cookies) {
[cookieStorage deleteCookie:cookie];
}
}
}
#pragma mark Accessors
- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val {
signIn_.networkLossTimeoutInterval = val;
}
- (NSTimeInterval)networkLossTimeoutInterval {
return signIn_.networkLossTimeoutInterval;
}
- (BOOL)shouldUseKeychain {
NSString *name = self.keychainItemName;
return ([name length] > 0);
}
- (BOOL)showsInitialActivityIndicator {
return (mustShowActivityIndicator_ == 1 || initialHTMLString_ == nil);
}
- (void)setShowsInitialActivityIndicator:(BOOL)flag {
mustShowActivityIndicator_ = (flag ? 1 : -1);
}
#pragma mark User Properties
- (void)setProperty:(id)obj forKey:(NSString *)key {
if (obj == nil) {
// User passed in nil, so delete the property
[properties_ removeObjectForKey:key];
} else {
// Be sure the property dictionary exists
if (properties_ == nil) {
[self setProperties:[NSMutableDictionary dictionary]];
}
[properties_ setObject:obj forKey:key];
}
}
- (id)propertyForKey:(NSString *)key {
id obj = [properties_ objectForKey:key];
// Be sure the returned pointer has the life of the autorelease pool,
// in case self is released immediately
return [[obj retain] autorelease];
}
#pragma mark SignIn callbacks
- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request {
// This is the signIn object's webRequest method, telling the controller
// to either display the request in the webview, or if the request is nil,
// to close the window.
//
// All web requests and all window closing goes through this routine
#if DEBUG
if (self.navigationController) {
if (self.navigationController.topViewController != self && request != nil) {
NSLog(@"Unexpected: Request to show, when already on top. request %@", [request URL]);
} else if(self.navigationController.topViewController != self && request == nil) {
NSLog(@"Unexpected: Request to pop, when not on top. request nil");
}
}
#endif
if (request != nil) {
const NSTimeInterval kJanuary2011 = 1293840000;
BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011);
if (isDateValid) {
// Display the request.
self.request = request;
// The app may prefer some html other than blank white to be displayed
// before the sign-in web page loads.
// The first fetch might be slow, so the client programmer may want
// to show a local "loading" message.
// On iOS 5+, UIWebView will ignore loadHTMLString: if it's followed by
// a loadRequest: call, so if there is a "loading" message we defer
// the loadRequest: until after after we've drawn the "loading" message.
//
// If there is no initial html string, we show the activity indicator
// unless the user set showsInitialActivityIndicator to NO; if there
// is an initial html string, we hide the indicator unless the user set
// showsInitialActivityIndicator to YES.
NSString *html = self.initialHTMLString;
if ([html length] > 0) {
[initialActivityIndicator_ setHidden:(mustShowActivityIndicator_ < 1)];
[self.webView loadHTMLString:html baseURL:nil];
} else {
[initialActivityIndicator_ setHidden:(mustShowActivityIndicator_ < 0)];
[self.webView loadRequest:request];
}
} else {
// clock date is invalid, so signing in would fail with an unhelpful error
// from the server. Warn the user in an html string showing a watch icon,
// question mark, and the system date and time. Hopefully this will clue
// in brighter users, or at least give them a clue when they report the
// problem to developers.
//
// Even better is for apps to check the system clock and show some more
// helpful, localized instructions for users; this is really a fallback.
NSString *const html = @"<html><body><div align=center><font size='7'>"
@"&#x231A; ?<br><i>System Clock Incorrect</i><br>%@"
@"</font></div></body></html>";
NSString *errHTML = [NSString stringWithFormat:html, [NSDate date]];
[[self webView] loadHTMLString:errHTML baseURL:nil];
}
} else {
// request was nil.
[self popView];
}
}
- (void)signIn:(GTMOAuth2SignIn *)signIn
finishedWithAuth:(GTMOAuth2Authentication *)auth
error:(NSError *)error {
if (!hasCalledFinished_) {
hasCalledFinished_ = YES;
if (error == nil) {
if (self.shouldUseKeychain) {
NSString *keychainItemName = self.keychainItemName;
if (auth.canAuthorize) {
// save the auth params in the keychain
CFTypeRef accessibility = self.keychainItemAccessibility;
[[self class] saveParamsToKeychainForName:keychainItemName
accessibility:accessibility
authentication:auth];
} else {
// remove the auth params from the keychain
[[self class] removeAuthFromKeychainForName:keychainItemName];
}
}
}
if (delegate_ && finishedSelector_) {
SEL sel = finishedSelector_;
NSMethodSignature *sig = [delegate_ methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setSelector:sel];
[invocation setTarget:delegate_];
[invocation setArgument:&self atIndex:2];
[invocation setArgument:&auth atIndex:3];
[invocation setArgument:&error atIndex:4];
[invocation invoke];
}
[delegate_ autorelease];
delegate_ = nil;
#if NS_BLOCKS_AVAILABLE
if (completionBlock_) {
completionBlock_(self, auth, error);
// release the block here to avoid a retain loop on the controller
[completionBlock_ autorelease];
completionBlock_ = nil;
}
#endif
}
}
- (void)moveWebViewFromUnderNavigationBar {
CGRect dontCare;
CGRect webFrame = self.view.bounds;
UINavigationBar *navigationBar = self.navigationController.navigationBar;
CGRectDivide(webFrame, &dontCare, &webFrame,
navigationBar.frame.size.height, CGRectMinYEdge);
[self.webView setFrame:webFrame];
}
// isTranslucent is defined in iPhoneOS 3.0 on.
- (BOOL)isNavigationBarTranslucent {
UINavigationBar *navigationBar = [[self navigationController] navigationBar];
BOOL isTranslucent =
([navigationBar respondsToSelector:@selector(isTranslucent)] &&
[navigationBar isTranslucent]);
return isTranslucent;
}
#pragma mark -
#pragma mark Protocol implementations
- (void)viewWillAppear:(BOOL)animated {
2013-08-11 04:08:25 +00:00
// See the comment on clearBrowserCookies in viewDidDisappear.
[self clearBrowserCookies];
if (!isViewShown_) {
isViewShown_ = YES;
if ([self isNavigationBarTranslucent]) {
[self moveWebViewFromUnderNavigationBar];
}
if (![signIn_ startSigningIn]) {
// Can't start signing in. We must pop our view.
// UIWebview needs time to stabilize. Animations need time to complete.
// We remove ourself from the view stack after that.
[self performSelector:@selector(popView)
withObject:nil
afterDelay:0.5
inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
}
}
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
didViewAppear_ = YES;
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
if (!didDismissSelf_) {
// We won't receive further webview delegate messages, so be sure the
// started loading notification is balanced, if necessary
[self notifyWithName:kGTMOAuth2WebViewStoppedLoading
webView:self.webView
kind:kGTMOAuth2WebViewCancelled];
// We are not popping ourselves, so presumably we are being popped by the
// navigation controller; tell the sign-in object to close up shop
//
// this will indirectly call our signIn:finishedWithAuth:error: method
// for us
[signIn_ windowWasClosed];
#if NS_BLOCKS_AVAILABLE
self.popViewBlock = nil;
#endif
}
2013-08-11 04:08:25 +00:00
[super viewWillDisappear:animated];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// prevent the next sign-in from showing in the WebView that the user is
2013-08-11 04:08:25 +00:00
// already signed in. It's possible for the WebView to set the cookies even
// after this, so we also clear them when the view first appears.
[self clearBrowserCookies];
}
- (void)viewDidLayoutSubviews {
// We don't call super's version of this method because
// -[UIViewController viewDidLayoutSubviews] is documented as a no-op, that
// didn't exist before iOS 5.
[initialActivityIndicator_ setCenter:[webView_ center]];
}
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
if (!hasDoneFinalRedirect_) {
hasDoneFinalRedirect_ = [signIn_ requestRedirectedToRequest:request];
if (hasDoneFinalRedirect_) {
// signIn has told the view to close
return NO;
}
}
return YES;
}
- (void)updateUI {
[backButton_ setEnabled:[[self webView] canGoBack]];
[forwardButton_ setEnabled:[[self webView] canGoForward]];
}
- (void)webViewDidStartLoad:(UIWebView *)webView {
[self notifyWithName:kGTMOAuth2WebViewStartedLoading
webView:webView
kind:nil];
[self updateUI];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[self notifyWithName:kGTMOAuth2WebViewStoppedLoading
webView:webView
kind:kGTMOAuth2WebViewFinished];
NSString *title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
if ([title length] > 0) {
[signIn_ titleChanged:title];
} else {
#if DEBUG
// Verify that Javascript is enabled
NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"1+1"];
NSAssert([result integerValue] == 2, @"GTMOAuth2: Javascript is required");
#endif
}
if (self.request && [self.initialHTMLString length] > 0) {
// The request was pending.
[self setInitialHTMLString:nil];
[self.webView loadRequest:self.request];
} else {
[initialActivityIndicator_ setHidden:YES];
[signIn_ cookiesChanged:[NSHTTPCookieStorage sharedHTTPCookieStorage]];
[self updateUI];
}
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
[self notifyWithName:kGTMOAuth2WebViewStoppedLoading
webView:webView
kind:kGTMOAuth2WebViewFailed];
// Tell the sign-in object that a load failed; if it was the authorization
// URL, it will pop the view and return an error to the delegate.
if (didViewAppear_) {
BOOL isUserInterruption = ([error code] == NSURLErrorCancelled
&& [[error domain] isEqual:NSURLErrorDomain]);
if (isUserInterruption) {
// Ignore this error:
// Users report that this error occurs when clicking too quickly on the
// accept button, before the page has completely loaded. Ignoring
// this error seems to provide a better experience than does immediately
// cancelling sign-in.
//
// This error also occurs whenever UIWebView is sent the stopLoading
// message, so if we ever send that message intentionally, we need to
// revisit this bypass.
return;
}
[signIn_ loadFailedWithError:error];
} else {
// UIWebview needs time to stabilize. Animations need time to complete.
[signIn_ performSelector:@selector(loadFailedWithError:)
withObject:error
afterDelay:0.5
inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
}
}
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000
// When running on a device with an OS version < 6, this gets called.
//
// Since it is never called in iOS 6 or greater, if your min deployment
// target is iOS6 or greater, then you don't need to have this method compiled
// into your app.
//
// When running on a device with an OS version 6 or greater, this code is
// not called. - (NSUInteger)supportedInterfaceOrientations; would be called,
// if it existed. Since it is absent,
// Allow the default orientations: All for iPad, all but upside down for iPhone.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
BOOL value = YES;
if (!isInsideShouldAutorotateToInterfaceOrientation_) {
isInsideShouldAutorotateToInterfaceOrientation_ = YES;
UIViewController *navigationController = [self navigationController];
if (navigationController != nil) {
value = [navigationController shouldAutorotateToInterfaceOrientation:interfaceOrientation];
} else {
value = [super shouldAutorotateToInterfaceOrientation:interfaceOrientation];
}
isInsideShouldAutorotateToInterfaceOrientation_ = NO;
}
return value;
}
#endif
@end
#pragma mark Common Code
@implementation GTMOAuth2Keychain
+ (GTMOAuth2Keychain *)defaultKeychain {
if (sDefaultKeychain == nil) {
sDefaultKeychain = [[self alloc] init];
}
return sDefaultKeychain;
}
// For unit tests: allow setting a mock object
+ (void)setDefaultKeychain:(GTMOAuth2Keychain *)keychain {
if (sDefaultKeychain != keychain) {
[sDefaultKeychain release];
sDefaultKeychain = [keychain retain];
}
}
- (NSString *)keyForService:(NSString *)service account:(NSString *)account {
return [NSString stringWithFormat:@"com.google.GTMOAuth.%@%@", service, account];
}
// The Keychain API isn't available on the iPhone simulator in SDKs before 3.0,
// so, on early simulators we use a fake API, that just writes, unencrypted, to
// NSUserDefaults.
#if TARGET_IPHONE_SIMULATOR && __IPHONE_OS_VERSION_MAX_ALLOWED < 30000
#pragma mark Simulator
// Simulator - just simulated, not secure.
- (NSString *)passwordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
NSString *result = nil;
if (0 < [service length] && 0 < [account length]) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *key = [self keyForService:service account:account];
result = [defaults stringForKey:key];
if (result == nil && error != NULL) {
*error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
code:kGTMOAuth2KeychainErrorNoPassword
userInfo:nil];
}
} else if (error != NULL) {
*error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
code:kGTMOAuth2KeychainErrorBadArguments
userInfo:nil];
}
return result;
}
// Simulator - just simulated, not secure.
- (BOOL)removePasswordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
BOOL didSucceed = NO;
if (0 < [service length] && 0 < [account length]) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *key = [self keyForService:service account:account];
[defaults removeObjectForKey:key];
[defaults synchronize];
} else if (error != NULL) {
*error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
code:kGTMOAuth2KeychainErrorBadArguments
userInfo:nil];
}
return didSucceed;
}
// Simulator - just simulated, not secure.
- (BOOL)setPassword:(NSString *)password
forService:(NSString *)service
accessibility:(CFTypeRef)accessibility
account:(NSString *)account
error:(NSError **)error {
BOOL didSucceed = NO;
if (0 < [password length] && 0 < [service length] && 0 < [account length]) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *key = [self keyForService:service account:account];
[defaults setObject:password forKey:key];
[defaults synchronize];
didSucceed = YES;
} else if (error != NULL) {
*error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
code:kGTMOAuth2KeychainErrorBadArguments
userInfo:nil];
}
return didSucceed;
}
#else // ! TARGET_IPHONE_SIMULATOR
#pragma mark Device
+ (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account {
NSMutableDictionary *query = [NSMutableDictionary dictionaryWithObjectsAndKeys:
(id)kSecClassGenericPassword, (id)kSecClass,
@"OAuth", (id)kSecAttrGeneric,
account, (id)kSecAttrAccount,
service, (id)kSecAttrService,
nil];
return query;
}
- (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account {
return [[self class] keychainQueryForService:service account:account];
}
// iPhone
- (NSString *)passwordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
OSStatus status = kGTMOAuth2KeychainErrorBadArguments;
NSString *result = nil;
if (0 < [service length] && 0 < [account length]) {
CFDataRef passwordData = NULL;
NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
[keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
[keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
status = SecItemCopyMatching((CFDictionaryRef)keychainQuery,
(CFTypeRef *)&passwordData);
if (status == noErr && 0 < [(NSData *)passwordData length]) {
result = [[[NSString alloc] initWithData:(NSData *)passwordData
encoding:NSUTF8StringEncoding] autorelease];
}
if (passwordData != NULL) {
CFRelease(passwordData);
}
}
if (status != noErr && error != NULL) {
*error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
code:status
userInfo:nil];
}
return result;
}
// iPhone
- (BOOL)removePasswordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
OSStatus status = kGTMOAuth2KeychainErrorBadArguments;
if (0 < [service length] && 0 < [account length]) {
NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
status = SecItemDelete((CFDictionaryRef)keychainQuery);
}
if (status != noErr && error != NULL) {
*error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
code:status
userInfo:nil];
}
return status == noErr;
}
// iPhone
- (BOOL)setPassword:(NSString *)password
forService:(NSString *)service
accessibility:(CFTypeRef)accessibility
account:(NSString *)account
error:(NSError **)error {
OSStatus status = kGTMOAuth2KeychainErrorBadArguments;
if (0 < [service length] && 0 < [account length]) {
[self removePasswordForService:service account:account error:nil];
if (0 < [password length]) {
NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
[keychainQuery setObject:passwordData forKey:(id)kSecValueData];
if (accessibility != NULL && &kSecAttrAccessible != NULL) {
[keychainQuery setObject:(id)accessibility
forKey:(id)kSecAttrAccessible];
}
status = SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}
}
if (status != noErr && error != NULL) {
*error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
code:status
userInfo:nil];
}
return status == noErr;
}
#endif // ! TARGET_IPHONE_SIMULATOR
@end
#endif // TARGET_OS_IPHONE
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES