/* 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 #import #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 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 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 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 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 { return [self authForGoogleFromKeychainForName:keychainItemName clientID:clientID clientSecret:clientSecret error:NULL]; } + (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName clientID:(NSString *)clientID clientSecret:(NSString *)clientSecret error:(NSError **)error { 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 error:error]; return auth; } #endif + (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName authentication:(GTMOAuth2Authentication *)newAuth error:(NSError **)error { newAuth.accessToken = nil; BOOL didGetTokens = NO; GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain]; NSString *password = [keychain passwordForService:keychainItemName account:kGTMOAuth2AccountName error:error]; 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 error:NULL]; } + (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName accessibility:(CFTypeRef)accessibility authentication:(GTMOAuth2Authentication *)auth error:(NSError **)error { [self removeAuthFromKeychainForName:keychainItemName]; // don't save unless we have a token that can really authorize requests if (![auth canAuthorize]) { if (error) { *error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain code:kGTMOAuth2ErrorTokenUnavailable userInfo:nil]; } 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:error]; } - (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 { [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 = @"
" @"⌚ ?
System Clock Incorrect
%@" @"
"; 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 error:NULL]; } 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 { // 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 } [super viewWillDisappear:animated]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; // prevent the next sign-in from showing in the WebView that the user is // 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