/core/externals/update-engine/externals/gdata-objectivec-client/Source/OAuth2/Touch/GTMOAuth2ViewControllerTouch.m
Objective C | 1098 lines | 800 code | 156 blank | 142 comment | 150 complexity | 18abc1d3fb6dced4cea8a24924b933ca MD5 | raw file
1/* Copyright (c) 2011 Google Inc. 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16// 17// GTMOAuth2ViewControllerTouch.m 18// 19 20#import <Foundation/Foundation.h> 21#import <Security/Security.h> 22 23#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES 24 25#if TARGET_OS_IPHONE 26 27#import "GTMOAuth2ViewControllerTouch.h" 28 29#import "GTMOAuth2SignIn.h" 30#import "GTMOAuth2Authentication.h" 31 32NSString *const kGTMOAuth2KeychainErrorDomain = @"com.google.GTMOAuthKeychain"; 33 34static NSString * const kGTMOAuth2AccountName = @"OAuth"; 35static GTMOAuth2Keychain* gGTMOAuth2DefaultKeychain = nil; 36 37@interface GTMOAuth2ViewControllerTouch() 38@property (nonatomic, copy) NSURLRequest *request; 39@property (nonatomic, copy) NSArray *savedCookies; 40@end 41 42@implementation GTMOAuth2ViewControllerTouch 43 44// IBOutlets 45@synthesize request = request_, 46 backButton = backButton_, 47 forwardButton = forwardButton_, 48 navButtonsView = navButtonsView_, 49 rightBarButtonItem = rightBarButtonItem_, 50 webView = webView_, 51 initialActivityIndicator = initialActivityIndicator_; 52 53@synthesize keychainItemName = keychainItemName_, 54 keychainItemAccessibility = keychainItemAccessibility_, 55 initialHTMLString = initialHTMLString_, 56 browserCookiesURL = browserCookiesURL_, 57 signIn = signIn_, 58 userData = userData_, 59 properties = properties_; 60 61#if NS_BLOCKS_AVAILABLE 62@synthesize popViewBlock = popViewBlock_; 63#endif 64 65#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT 66+ (id)controllerWithScope:(NSString *)scope 67 clientID:(NSString *)clientID 68 clientSecret:(NSString *)clientSecret 69 keychainItemName:(NSString *)keychainItemName 70 delegate:(id)delegate 71 finishedSelector:(SEL)finishedSelector { 72 return [[[self alloc] initWithScope:scope 73 clientID:clientID 74 clientSecret:clientSecret 75 keychainItemName:keychainItemName 76 delegate:delegate 77 finishedSelector:finishedSelector] autorelease]; 78} 79 80- (id)initWithScope:(NSString *)scope 81 clientID:(NSString *)clientID 82 clientSecret:(NSString *)clientSecret 83 keychainItemName:(NSString *)keychainItemName 84 delegate:(id)delegate 85 finishedSelector:(SEL)finishedSelector { 86 // convenient entry point for Google authentication 87 88 Class signInClass = [[self class] signInClass]; 89 90 GTMOAuth2Authentication *auth; 91 auth = [signInClass standardGoogleAuthenticationForScope:scope 92 clientID:clientID 93 clientSecret:clientSecret]; 94 NSURL *authorizationURL = [signInClass googleAuthorizationURL]; 95 return [self initWithAuthentication:auth 96 authorizationURL:authorizationURL 97 keychainItemName:keychainItemName 98 delegate:delegate 99 finishedSelector:finishedSelector]; 100} 101 102#if NS_BLOCKS_AVAILABLE 103 104+ (id)controllerWithScope:(NSString *)scope 105 clientID:(NSString *)clientID 106 clientSecret:(NSString *)clientSecret 107 keychainItemName:(NSString *)keychainItemName 108 completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler { 109 return [[[self alloc] initWithScope:scope 110 clientID:clientID 111 clientSecret:clientSecret 112 keychainItemName:keychainItemName 113 completionHandler:handler] autorelease]; 114} 115 116- (id)initWithScope:(NSString *)scope 117 clientID:(NSString *)clientID 118 clientSecret:(NSString *)clientSecret 119 keychainItemName:(NSString *)keychainItemName 120 completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler { 121 // convenient entry point for Google authentication 122 123 Class signInClass = [[self class] signInClass]; 124 125 GTMOAuth2Authentication *auth; 126 auth = [signInClass standardGoogleAuthenticationForScope:scope 127 clientID:clientID 128 clientSecret:clientSecret]; 129 NSURL *authorizationURL = [signInClass googleAuthorizationURL]; 130 self = [self initWithAuthentication:auth 131 authorizationURL:authorizationURL 132 keychainItemName:keychainItemName 133 delegate:nil 134 finishedSelector:NULL]; 135 if (self) { 136 completionBlock_ = [handler copy]; 137 } 138 return self; 139} 140 141#endif // NS_BLOCKS_AVAILABLE 142#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT 143 144+ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth 145 authorizationURL:(NSURL *)authorizationURL 146 keychainItemName:(NSString *)keychainItemName 147 delegate:(id)delegate 148 finishedSelector:(SEL)finishedSelector { 149 return [[[self alloc] initWithAuthentication:auth 150 authorizationURL:authorizationURL 151 keychainItemName:keychainItemName 152 delegate:delegate 153 finishedSelector:finishedSelector] autorelease]; 154} 155 156- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth 157 authorizationURL:(NSURL *)authorizationURL 158 keychainItemName:(NSString *)keychainItemName 159 delegate:(id)delegate 160 finishedSelector:(SEL)finishedSelector { 161 162 NSString *nibName = [[self class] authNibName]; 163 NSBundle *nibBundle = [[self class] authNibBundle]; 164 165 self = [super initWithNibName:nibName bundle:nibBundle]; 166 if (self != nil) { 167 delegate_ = [delegate retain]; 168 finishedSelector_ = finishedSelector; 169 170 Class signInClass = [[self class] signInClass]; 171 172 // use the supplied auth and OAuth endpoint URLs 173 signIn_ = [[signInClass alloc] initWithAuthentication:auth 174 authorizationURL:authorizationURL 175 delegate:self 176 webRequestSelector:@selector(signIn:displayRequest:) 177 finishedSelector:@selector(signIn:finishedWithAuth:error:)]; 178 179 // if the user is signing in to a Google service, we'll delete the 180 // Google authentication browser cookies upon completion 181 // 182 // for other service domains, or to disable clearing of the cookies, 183 // set the browserCookiesURL property explicitly 184 NSString *authorizationHost = [signIn_.authorizationURL host]; 185 if ([authorizationHost hasSuffix:@".google.com"]) { 186 NSString *urlStr = [NSString stringWithFormat:@"https://%@/", 187 authorizationHost]; 188 NSURL *cookiesURL = [NSURL URLWithString:urlStr]; 189 [self setBrowserCookiesURL:cookiesURL]; 190 } 191 192 [self setKeychainItemName:keychainItemName]; 193 194 savedCookiePolicy_ = (NSHTTPCookieAcceptPolicy)NSUIntegerMax; 195 } 196 return self; 197} 198 199#if NS_BLOCKS_AVAILABLE 200+ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth 201 authorizationURL:(NSURL *)authorizationURL 202 keychainItemName:(NSString *)keychainItemName 203 completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler { 204 return [[[self alloc] initWithAuthentication:auth 205 authorizationURL:authorizationURL 206 keychainItemName:keychainItemName 207 completionHandler:handler] autorelease]; 208} 209 210- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth 211 authorizationURL:(NSURL *)authorizationURL 212 keychainItemName:(NSString *)keychainItemName 213 completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler { 214 // fall back to the non-blocks init 215 self = [self initWithAuthentication:auth 216 authorizationURL:authorizationURL 217 keychainItemName:keychainItemName 218 delegate:nil 219 finishedSelector:NULL]; 220 if (self) { 221 completionBlock_ = [handler copy]; 222 } 223 return self; 224} 225#endif 226 227- (void)dealloc { 228 [webView_ setDelegate:nil]; 229 230 [backButton_ release]; 231 [forwardButton_ release]; 232 [initialActivityIndicator_ release]; 233 [navButtonsView_ release]; 234 [rightBarButtonItem_ release]; 235 [webView_ stopLoading]; 236 [webView_ release]; 237 [signIn_ release]; 238 [request_ release]; 239 [delegate_ release]; 240#if NS_BLOCKS_AVAILABLE 241 [completionBlock_ release]; 242 [popViewBlock_ release]; 243#endif 244 [keychainItemName_ release]; 245 [initialHTMLString_ release]; 246 [browserCookiesURL_ release]; 247 [userData_ release]; 248 [properties_ release]; 249 250 [super dealloc]; 251} 252 253+ (NSString *)authNibName { 254 // subclasses may override this to specify a custom nib name 255 return @"GTMOAuth2ViewTouch"; 256} 257 258+ (NSBundle *)authNibBundle { 259 // subclasses may override this to specify a custom nib bundle 260 return nil; 261} 262 263#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT 264+ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName 265 clientID:(NSString *)clientID 266 clientSecret:(NSString *)clientSecret { 267 return [self authForGoogleFromKeychainForName:keychainItemName 268 clientID:clientID 269 clientSecret:clientSecret 270 error:NULL]; 271} 272 273+ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName 274 clientID:(NSString *)clientID 275 clientSecret:(NSString *)clientSecret 276 error:(NSError **)error { 277 Class signInClass = [self signInClass]; 278 NSURL *tokenURL = [signInClass googleTokenURL]; 279 NSString *redirectURI = [signInClass nativeClientRedirectURI]; 280 281 GTMOAuth2Authentication *auth; 282 auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle 283 tokenURL:tokenURL 284 redirectURI:redirectURI 285 clientID:clientID 286 clientSecret:clientSecret]; 287 [[self class] authorizeFromKeychainForName:keychainItemName 288 authentication:auth 289 error:error]; 290 return auth; 291} 292 293#endif 294 295+ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName 296 authentication:(GTMOAuth2Authentication *)newAuth 297 error:(NSError **)error { 298 newAuth.accessToken = nil; 299 300 BOOL didGetTokens = NO; 301 GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain]; 302 NSString *password = [keychain passwordForService:keychainItemName 303 account:kGTMOAuth2AccountName 304 error:error]; 305 if (password != nil) { 306 [newAuth setKeysForResponseString:password]; 307 didGetTokens = YES; 308 } 309 return didGetTokens; 310} 311 312+ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName { 313 GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain]; 314 return [keychain removePasswordForService:keychainItemName 315 account:kGTMOAuth2AccountName 316 error:nil]; 317} 318 319+ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName 320 authentication:(GTMOAuth2Authentication *)auth { 321 return [self saveParamsToKeychainForName:keychainItemName 322 accessibility:NULL 323 authentication:auth 324 error:NULL]; 325} 326 327+ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName 328 accessibility:(CFTypeRef)accessibility 329 authentication:(GTMOAuth2Authentication *)auth 330 error:(NSError **)error { 331 [self removeAuthFromKeychainForName:keychainItemName]; 332 // don't save unless we have a token that can really authorize requests 333 if (![auth canAuthorize]) { 334 if (error) { 335 *error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain 336 code:kGTMOAuth2ErrorTokenUnavailable 337 userInfo:nil]; 338 } 339 return NO; 340 } 341 342 if (accessibility == NULL 343 && &kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly != NULL) { 344 accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly; 345 } 346 347 // make a response string containing the values we want to save 348 NSString *password = [auth persistenceResponseString]; 349 GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain]; 350 return [keychain setPassword:password 351 forService:keychainItemName 352 accessibility:accessibility 353 account:kGTMOAuth2AccountName 354 error:error]; 355} 356 357- (void)loadView { 358 NSString *nibPath = nil; 359 NSBundle *nibBundle = [self nibBundle]; 360 if (nibBundle == nil) { 361 nibBundle = [NSBundle mainBundle]; 362 } 363 NSString *nibName = self.nibName; 364 if (nibName != nil) { 365 nibPath = [nibBundle pathForResource:nibName ofType:@"nib"]; 366 } 367 if (nibPath != nil && [[NSFileManager defaultManager] fileExistsAtPath:nibPath]) { 368 [super loadView]; 369 } else { 370 // One of the requirements of loadView is that a valid view object is set to 371 // self.view upon completion. Otherwise, subclasses that attempt to 372 // access self.view after calling [super loadView] will enter an infinite 373 // loop due to the fact that UIViewController's -view accessor calls 374 // loadView when self.view is nil. 375 self.view = [[[UIView alloc] init] autorelease]; 376 377#if DEBUG 378 NSLog(@"missing %@.nib", nibName); 379#endif 380 } 381} 382 383 384- (void)viewDidLoad { 385 [self setUpNavigation]; 386} 387 388- (void)setUpNavigation { 389 rightBarButtonItem_.customView = navButtonsView_; 390 self.navigationItem.rightBarButtonItem = rightBarButtonItem_; 391} 392 393- (void)popView { 394#if NS_BLOCKS_AVAILABLE 395 void (^popViewBlock)() = self.popViewBlock; 396#else 397 id popViewBlock = nil; 398#endif 399 400 if (popViewBlock || self.navigationController.topViewController == self) { 401 if (!self.view.hidden) { 402 // Set the flag to our viewWillDisappear method so it knows 403 // this is a disappearance initiated by the sign-in object, 404 // not the user cancelling via the navigation controller 405 didDismissSelf_ = YES; 406 407 if (popViewBlock) { 408#if NS_BLOCKS_AVAILABLE 409 popViewBlock(); 410 self.popViewBlock = nil; 411#endif 412 } else { 413 [self.navigationController popViewControllerAnimated:YES]; 414 } 415 self.view.hidden = YES; 416 } 417 } 418} 419 420- (void)notifyWithName:(NSString *)name 421 webView:(UIWebView *)webView 422 kind:(NSString *)kind { 423 BOOL isStarting = [name isEqual:kGTMOAuth2WebViewStartedLoading]; 424 if (hasNotifiedWebViewStartedLoading_ == isStarting) { 425 // Duplicate notification 426 // 427 // UIWebView's delegate methods are so unbalanced that there's little 428 // point trying to keep a count, as it could easily end up stuck greater 429 // than zero. 430 // 431 // We don't really have a way to track the starts and stops of 432 // subframe loads, too, as the webView in the notification is always 433 // for the topmost request. 434 return; 435 } 436 hasNotifiedWebViewStartedLoading_ = isStarting; 437 438 // Notification for webview load starting and stopping 439 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: 440 webView, kGTMOAuth2WebViewKey, 441 kind, kGTMOAuth2WebViewStopKindKey, // kind may be nil 442 nil]; 443 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 444 [nc postNotificationName:name 445 object:self 446 userInfo:dict]; 447} 448 449- (void)cancelSigningIn { 450 // The application has explicitly asked us to cancel signing in 451 // (so no further callback is required) 452 hasCalledFinished_ = YES; 453 454 [delegate_ autorelease]; 455 delegate_ = nil; 456 457#if NS_BLOCKS_AVAILABLE 458 [completionBlock_ autorelease]; 459 completionBlock_ = nil; 460#endif 461 462 // The sign-in object's cancel method will close the window 463 [signIn_ cancelSigningIn]; 464 hasDoneFinalRedirect_ = YES; 465} 466 467static Class gSignInClass = Nil; 468 469+ (Class)signInClass { 470 if (gSignInClass == Nil) { 471 gSignInClass = [GTMOAuth2SignIn class]; 472 } 473 return gSignInClass; 474} 475 476+ (void)setSignInClass:(Class)theClass { 477 gSignInClass = theClass; 478} 479 480#pragma mark Token Revocation 481 482#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT 483+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth { 484 [[self signInClass] revokeTokenForGoogleAuthentication:auth]; 485} 486#endif 487 488#pragma mark Browser Cookies 489 490- (GTMOAuth2Authentication *)authentication { 491 return self.signIn.authentication; 492} 493 494- (void)saveBrowserCookies { 495 NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 496 self.savedCookies = [cookieStorage cookies]; 497} 498 499- (void)restoreBrowserCookies { 500 // Remove all current cookies and restore the saved array. 501 NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 502 NSHTTPCookieAcceptPolicy savedPolicy = [cookieStorage cookieAcceptPolicy]; 503 [cookieStorage setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways]; 504 505 for (NSHTTPCookie *cookie in [cookieStorage cookies]) { 506 [cookieStorage deleteCookie:cookie]; 507 } 508 for (NSHTTPCookie *cookie in self.savedCookies) { 509 [cookieStorage setCookie:cookie]; 510 } 511 self.savedCookies = nil; 512 513 [cookieStorage setCookieAcceptPolicy:savedPolicy]; 514} 515 516- (void)clearSpecifiedBrowserCookies { 517 // If browserCookiesURL is non-nil, then get cookies for that URL 518 // and delete them from the common application cookie storage 519 NSURL *cookiesURL = [self browserCookiesURL]; 520 if (cookiesURL) { 521 NSHTTPCookieStorage *cookieStorage; 522 523 cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 524 NSArray *cookies = [cookieStorage cookiesForURL:cookiesURL]; 525 526 for (NSHTTPCookie *cookie in cookies) { 527 [cookieStorage deleteCookie:cookie]; 528 } 529 } 530} 531 532#pragma mark Accessors 533 534- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val { 535 signIn_.networkLossTimeoutInterval = val; 536} 537 538- (NSTimeInterval)networkLossTimeoutInterval { 539 return signIn_.networkLossTimeoutInterval; 540} 541 542- (BOOL)shouldUseKeychain { 543 NSString *name = self.keychainItemName; 544 return ([name length] > 0); 545} 546 547- (BOOL)showsInitialActivityIndicator { 548 return (mustShowActivityIndicator_ == 1 || initialHTMLString_ == nil); 549} 550 551- (void)setShowsInitialActivityIndicator:(BOOL)flag { 552 mustShowActivityIndicator_ = (flag ? 1 : -1); 553} 554 555#pragma mark User Properties 556 557- (void)setProperty:(id)obj forKey:(NSString *)key { 558 if (obj == nil) { 559 // User passed in nil, so delete the property 560 [properties_ removeObjectForKey:key]; 561 } else { 562 // Be sure the property dictionary exists 563 if (properties_ == nil) { 564 [self setProperties:[NSMutableDictionary dictionary]]; 565 } 566 [properties_ setObject:obj forKey:key]; 567 } 568} 569 570- (id)propertyForKey:(NSString *)key { 571 id obj = [properties_ objectForKey:key]; 572 573 // Be sure the returned pointer has the life of the autorelease pool, 574 // in case self is released immediately 575 return [[obj retain] autorelease]; 576} 577 578#pragma mark SignIn callbacks 579 580- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request { 581 // This is the signIn object's webRequest method, telling the controller 582 // to either display the request in the webview, or if the request is nil, 583 // to close the window. 584 // 585 // All web requests and all window closing goes through this routine 586 587#if DEBUG 588 if (self.navigationController) { 589 if (self.navigationController.topViewController != self && request != nil) { 590 NSLog(@"Unexpected: Request to show, when already on top. request %@", [request URL]); 591 } else if(self.navigationController.topViewController != self && request == nil) { 592 NSLog(@"Unexpected: Request to pop, when not on top. request nil"); 593 } 594 } 595#endif 596 597 if (request != nil) { 598 const NSTimeInterval kJanuary2011 = 1293840000; 599 BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011); 600 if (isDateValid) { 601 // Display the request. 602 self.request = request; 603 // The app may prefer some html other than blank white to be displayed 604 // before the sign-in web page loads. 605 // The first fetch might be slow, so the client programmer may want 606 // to show a local "loading" message. 607 // On iOS 5+, UIWebView will ignore loadHTMLString: if it's followed by 608 // a loadRequest: call, so if there is a "loading" message we defer 609 // the loadRequest: until after after we've drawn the "loading" message. 610 // 611 // If there is no initial html string, we show the activity indicator 612 // unless the user set showsInitialActivityIndicator to NO; if there 613 // is an initial html string, we hide the indicator unless the user set 614 // showsInitialActivityIndicator to YES. 615 NSString *html = self.initialHTMLString; 616 if ([html length] > 0) { 617 [initialActivityIndicator_ setHidden:(mustShowActivityIndicator_ < 1)]; 618 [self.webView loadHTMLString:html baseURL:nil]; 619 } else { 620 [initialActivityIndicator_ setHidden:(mustShowActivityIndicator_ < 0)]; 621 [self.webView loadRequest:request]; 622 } 623 } else { 624 // clock date is invalid, so signing in would fail with an unhelpful error 625 // from the server. Warn the user in an html string showing a watch icon, 626 // question mark, and the system date and time. Hopefully this will clue 627 // in brighter users, or at least give them a clue when they report the 628 // problem to developers. 629 // 630 // Even better is for apps to check the system clock and show some more 631 // helpful, localized instructions for users; this is really a fallback. 632 NSString *const html = @"<html><body><div align=center><font size='7'>" 633 @"⌚ ?<br><i>System Clock Incorrect</i><br>%@" 634 @"</font></div></body></html>"; 635 NSString *errHTML = [NSString stringWithFormat:html, [NSDate date]]; 636 637 [[self webView] loadHTMLString:errHTML baseURL:nil]; 638 } 639 } else { 640 // request was nil. 641 [self popView]; 642 } 643} 644 645- (void)signIn:(GTMOAuth2SignIn *)signIn 646 finishedWithAuth:(GTMOAuth2Authentication *)auth 647 error:(NSError *)error { 648 if (!hasCalledFinished_) { 649 hasCalledFinished_ = YES; 650 651 if (error == nil) { 652 if (self.shouldUseKeychain) { 653 NSString *keychainItemName = self.keychainItemName; 654 if (auth.canAuthorize) { 655 // save the auth params in the keychain 656 CFTypeRef accessibility = self.keychainItemAccessibility; 657 [[self class] saveParamsToKeychainForName:keychainItemName 658 accessibility:accessibility 659 authentication:auth 660 error:NULL]; 661 } else { 662 // remove the auth params from the keychain 663 [[self class] removeAuthFromKeychainForName:keychainItemName]; 664 } 665 } 666 } 667 668 if (delegate_ && finishedSelector_) { 669 SEL sel = finishedSelector_; 670 NSMethodSignature *sig = [delegate_ methodSignatureForSelector:sel]; 671 NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; 672 [invocation setSelector:sel]; 673 [invocation setTarget:delegate_]; 674 [invocation setArgument:&self atIndex:2]; 675 [invocation setArgument:&auth atIndex:3]; 676 [invocation setArgument:&error atIndex:4]; 677 [invocation invoke]; 678 } 679 680 [delegate_ autorelease]; 681 delegate_ = nil; 682 683#if NS_BLOCKS_AVAILABLE 684 if (completionBlock_) { 685 completionBlock_(self, auth, error); 686 687 // release the block here to avoid a retain loop on the controller 688 [completionBlock_ autorelease]; 689 completionBlock_ = nil; 690 } 691#endif 692 } 693} 694 695- (void)moveWebViewFromUnderNavigationBar { 696 CGRect dontCare; 697 CGRect webFrame = self.view.bounds; 698 UINavigationBar *navigationBar = self.navigationController.navigationBar; 699 CGRectDivide(webFrame, &dontCare, &webFrame, 700 navigationBar.frame.size.height, CGRectMinYEdge); 701 [self.webView setFrame:webFrame]; 702} 703 704// isTranslucent is defined in iPhoneOS 3.0 on. 705- (BOOL)isNavigationBarTranslucent { 706 UINavigationBar *navigationBar = [[self navigationController] navigationBar]; 707 BOOL isTranslucent = 708 ([navigationBar respondsToSelector:@selector(isTranslucent)] && 709 [navigationBar isTranslucent]); 710 return isTranslucent; 711} 712 713#pragma mark - 714#pragma mark Protocol implementations 715 716- (void)viewWillAppear:(BOOL)animated { 717 // See the comment on clearBrowserCookies in viewWillDisappear. 718 [self saveBrowserCookies]; 719 [self clearSpecifiedBrowserCookies]; 720 721 if (!isViewShown_) { 722 isViewShown_ = YES; 723 if ([self isNavigationBarTranslucent]) { 724 [self moveWebViewFromUnderNavigationBar]; 725 } 726 if (![signIn_ startSigningIn]) { 727 // Can't start signing in. We must pop our view. 728 // UIWebview needs time to stabilize. Animations need time to complete. 729 // We remove ourself from the view stack after that. 730 [self performSelector:@selector(popView) 731 withObject:nil 732 afterDelay:0.5 733 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; 734 } 735 736 // Work around iOS 7.0 bug described in https://devforums.apple.com/thread/207323 by temporarily 737 // setting our cookie storage policy to be permissive enough to keep the sign-in server 738 // satisfied, just in case the app inherited from Safari a policy that blocks all cookies. 739 NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 740 NSHTTPCookieAcceptPolicy policy = [storage cookieAcceptPolicy]; 741 if (policy == NSHTTPCookieAcceptPolicyNever) { 742 savedCookiePolicy_ = policy; 743 [storage setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain]; 744 } 745 } 746 747 [super viewWillAppear:animated]; 748} 749 750- (void)viewDidAppear:(BOOL)animated { 751 didViewAppear_ = YES; 752 [super viewDidAppear:animated]; 753} 754 755- (void)viewWillDisappear:(BOOL)animated { 756 if (!didDismissSelf_) { 757 // We won't receive further webview delegate messages, so be sure the 758 // started loading notification is balanced, if necessary 759 [self notifyWithName:kGTMOAuth2WebViewStoppedLoading 760 webView:self.webView 761 kind:kGTMOAuth2WebViewCancelled]; 762 763 // We are not popping ourselves, so presumably we are being popped by the 764 // navigation controller; tell the sign-in object to close up shop 765 // 766 // this will indirectly call our signIn:finishedWithAuth:error: method 767 // for us 768 [signIn_ windowWasClosed]; 769 770#if NS_BLOCKS_AVAILABLE 771 self.popViewBlock = nil; 772#endif 773 } 774 775 [self restoreBrowserCookies]; 776 777 if (savedCookiePolicy_ != (NSHTTPCookieAcceptPolicy)NSUIntegerMax) { 778 NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 779 [storage setCookieAcceptPolicy:savedCookiePolicy_]; 780 savedCookiePolicy_ = (NSHTTPCookieAcceptPolicy)NSUIntegerMax; 781 } 782 783 [super viewWillDisappear:animated]; 784} 785 786- (void)viewDidLayoutSubviews { 787 // We don't call super's version of this method because 788 // -[UIViewController viewDidLayoutSubviews] is documented as a no-op, that 789 // didn't exist before iOS 5. 790 [initialActivityIndicator_ setCenter:[webView_ center]]; 791} 792 793- (BOOL)webView:(UIWebView *)webView 794 shouldStartLoadWithRequest:(NSURLRequest *)request 795 navigationType:(UIWebViewNavigationType)navigationType { 796 797 if (!hasDoneFinalRedirect_) { 798 hasDoneFinalRedirect_ = [signIn_ requestRedirectedToRequest:request]; 799 if (hasDoneFinalRedirect_) { 800 // signIn has told the view to close 801 return NO; 802 } 803 } 804 return YES; 805} 806 807- (void)updateUI { 808 [backButton_ setEnabled:[[self webView] canGoBack]]; 809 [forwardButton_ setEnabled:[[self webView] canGoForward]]; 810} 811 812- (void)webViewDidStartLoad:(UIWebView *)webView { 813 [self notifyWithName:kGTMOAuth2WebViewStartedLoading 814 webView:webView 815 kind:nil]; 816 [self updateUI]; 817} 818 819- (void)webViewDidFinishLoad:(UIWebView *)webView { 820 [self notifyWithName:kGTMOAuth2WebViewStoppedLoading 821 webView:webView 822 kind:kGTMOAuth2WebViewFinished]; 823 824 NSString *title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"]; 825 if ([title length] > 0) { 826 [signIn_ titleChanged:title]; 827 } else { 828#if DEBUG 829 // Verify that Javascript is enabled 830 NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"1+1"]; 831 NSAssert([result integerValue] == 2, @"GTMOAuth2: Javascript is required"); 832#endif 833 } 834 835 if (self.request && [self.initialHTMLString length] > 0) { 836 // The request was pending. 837 [self setInitialHTMLString:nil]; 838 [self.webView loadRequest:self.request]; 839 } else { 840 [initialActivityIndicator_ setHidden:YES]; 841 [signIn_ cookiesChanged:[NSHTTPCookieStorage sharedHTTPCookieStorage]]; 842 843 [self updateUI]; 844 } 845} 846 847- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { 848 [self notifyWithName:kGTMOAuth2WebViewStoppedLoading 849 webView:webView 850 kind:kGTMOAuth2WebViewFailed]; 851 852 // Tell the sign-in object that a load failed; if it was the authorization 853 // URL, it will pop the view and return an error to the delegate. 854 if (didViewAppear_) { 855 BOOL isUserInterruption = ([error code] == NSURLErrorCancelled 856 && [[error domain] isEqual:NSURLErrorDomain]); 857 if (isUserInterruption) { 858 // Ignore this error: 859 // Users report that this error occurs when clicking too quickly on the 860 // accept button, before the page has completely loaded. Ignoring 861 // this error seems to provide a better experience than does immediately 862 // cancelling sign-in. 863 // 864 // This error also occurs whenever UIWebView is sent the stopLoading 865 // message, so if we ever send that message intentionally, we need to 866 // revisit this bypass. 867 return; 868 } 869 870 [signIn_ loadFailedWithError:error]; 871 } else { 872 // UIWebview needs time to stabilize. Animations need time to complete. 873 [signIn_ performSelector:@selector(loadFailedWithError:) 874 withObject:error 875 afterDelay:0.5 876 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; 877 } 878} 879 880#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000 881// When running on a device with an OS version < 6, this gets called. 882// 883// Since it is never called in iOS 6 or greater, if your min deployment 884// target is iOS6 or greater, then you don't need to have this method compiled 885// into your app. 886// 887// When running on a device with an OS version 6 or greater, this code is 888// not called. - (NSUInteger)supportedInterfaceOrientations; would be called, 889// if it existed. Since it is absent, 890// Allow the default orientations: All for iPad, all but upside down for iPhone. 891- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { 892 BOOL value = YES; 893 if (!isInsideShouldAutorotateToInterfaceOrientation_) { 894 isInsideShouldAutorotateToInterfaceOrientation_ = YES; 895 UIViewController *navigationController = [self navigationController]; 896 if (navigationController != nil) { 897 value = [navigationController shouldAutorotateToInterfaceOrientation:interfaceOrientation]; 898 } else { 899 value = [super shouldAutorotateToInterfaceOrientation:interfaceOrientation]; 900 } 901 isInsideShouldAutorotateToInterfaceOrientation_ = NO; 902 } 903 return value; 904} 905#endif 906 907 908@end 909 910 911#pragma mark Common Code 912 913@implementation GTMOAuth2Keychain 914 915+ (GTMOAuth2Keychain *)defaultKeychain { 916 if (gGTMOAuth2DefaultKeychain == nil) { 917 gGTMOAuth2DefaultKeychain = [[self alloc] init]; 918 } 919 return gGTMOAuth2DefaultKeychain; 920} 921 922 923// For unit tests: allow setting a mock object 924+ (void)setDefaultKeychain:(GTMOAuth2Keychain *)keychain { 925 if (gGTMOAuth2DefaultKeychain != keychain) { 926 [gGTMOAuth2DefaultKeychain release]; 927 gGTMOAuth2DefaultKeychain = [keychain retain]; 928 } 929} 930 931- (NSString *)keyForService:(NSString *)service account:(NSString *)account { 932 return [NSString stringWithFormat:@"com.google.GTMOAuth.%@%@", service, account]; 933} 934 935// The Keychain API isn't available on the iPhone simulator in SDKs before 3.0, 936// so, on early simulators we use a fake API, that just writes, unencrypted, to 937// NSUserDefaults. 938#if TARGET_IPHONE_SIMULATOR && __IPHONE_OS_VERSION_MAX_ALLOWED < 30000 939#pragma mark Simulator 940 941// Simulator - just simulated, not secure. 942- (NSString *)passwordForService:(NSString *)service account:(NSString *)account error:(NSError **)error { 943 NSString *result = nil; 944 if (0 < [service length] && 0 < [account length]) { 945 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 946 NSString *key = [self keyForService:service account:account]; 947 result = [defaults stringForKey:key]; 948 if (result == nil && error != NULL) { 949 *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain 950 code:kGTMOAuth2KeychainErrorNoPassword 951 userInfo:nil]; 952 } 953 } else if (error != NULL) { 954 *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain 955 code:kGTMOAuth2KeychainErrorBadArguments 956 userInfo:nil]; 957 } 958 return result; 959 960} 961 962 963// Simulator - just simulated, not secure. 964- (BOOL)removePasswordForService:(NSString *)service account:(NSString *)account error:(NSError **)error { 965 BOOL didSucceed = NO; 966 if (0 < [service length] && 0 < [account length]) { 967 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 968 NSString *key = [self keyForService:service account:account]; 969 [defaults removeObjectForKey:key]; 970 [defaults synchronize]; 971 } else if (error != NULL) { 972 *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain 973 code:kGTMOAuth2KeychainErrorBadArguments 974 userInfo:nil]; 975 } 976 return didSucceed; 977} 978 979// Simulator - just simulated, not secure. 980- (BOOL)setPassword:(NSString *)password 981 forService:(NSString *)service 982 accessibility:(CFTypeRef)accessibility 983 account:(NSString *)account 984 error:(NSError **)error { 985 BOOL didSucceed = NO; 986 if (0 < [password length] && 0 < [service length] && 0 < [account length]) { 987 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 988 NSString *key = [self keyForService:service account:account]; 989 [defaults setObject:password forKey:key]; 990 [defaults synchronize]; 991 didSucceed = YES; 992 } else if (error != NULL) { 993 *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain 994 code:kGTMOAuth2KeychainErrorBadArguments 995 userInfo:nil]; 996 } 997 return didSucceed; 998} 999 1000#else // ! TARGET_IPHONE_SIMULATOR 1001#pragma mark Device 1002 1003+ (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account { 1004 NSMutableDictionary *query = [NSMutableDictionary dictionaryWithObjectsAndKeys: 1005 (id)kSecClassGenericPassword, (id)kSecClass, 1006 @"OAuth", (id)kSecAttrGeneric, 1007 account, (id)kSecAttrAccount, 1008 service, (id)kSecAttrService, 1009 nil]; 1010 return query; 1011} 1012 1013- (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account { 1014 return [[self class] keychainQueryForService:service account:account]; 1015} 1016 1017 1018 1019// iPhone 1020- (NSString *)passwordForService:(NSString *)service account:(NSString *)account error:(NSError **)error { 1021 OSStatus status = kGTMOAuth2KeychainErrorBadArguments; 1022 NSString *result = nil; 1023 if (0 < [service length] && 0 < [account length]) { 1024 CFDataRef passwordData = NULL; 1025 NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account]; 1026 [keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData]; 1027 [keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit]; 1028 1029 status = SecItemCopyMatching((CFDictionaryRef)keychainQuery, 1030 (CFTypeRef *)&passwordData); 1031 if (status == noErr && 0 < [(NSData *)passwordData length]) { 1032 result = [[[NSString alloc] initWithData:(NSData *)passwordData 1033 encoding:NSUTF8StringEncoding] autorelease]; 1034 } 1035 if (passwordData != NULL) { 1036 CFRelease(passwordData); 1037 } 1038 } 1039 if (status != noErr && error != NULL) { 1040 *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain 1041 code:status 1042 userInfo:nil]; 1043 } 1044 return result; 1045} 1046 1047 1048// iPhone 1049- (BOOL)removePasswordForService:(NSString *)service account:(NSString *)account error:(NSError **)error { 1050 OSStatus status = kGTMOAuth2KeychainErrorBadArguments; 1051 if (0 < [service length] && 0 < [account length]) { 1052 NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account]; 1053 status = SecItemDelete((CFDictionaryRef)keychainQuery); 1054 } 1055 if (status != noErr && error != NULL) { 1056 *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain 1057 code:status 1058 userInfo:nil]; 1059 } 1060 return status == noErr; 1061} 1062 1063// iPhone 1064- (BOOL)setPassword:(NSString *)password 1065 forService:(NSString *)service 1066 accessibility:(CFTypeRef)accessibility 1067 account:(NSString *)account 1068 error:(NSError **)error { 1069 OSStatus status = kGTMOAuth2KeychainErrorBadArguments; 1070 if (0 < [service length] && 0 < [account length]) { 1071 [self removePasswordForService:service account:account error:nil]; 1072 if (0 < [password length]) { 1073 NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account]; 1074 NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; 1075 [keychainQuery setObject:passwordData forKey:(id)kSecValueData]; 1076 1077 if (accessibility != NULL && &kSecAttrAccessible != NULL) { 1078 [keychainQuery setObject:(id)accessibility 1079 forKey:(id)kSecAttrAccessible]; 1080 } 1081 status = SecItemAdd((CFDictionaryRef)keychainQuery, NULL); 1082 } 1083 } 1084 if (status != noErr && error != NULL) { 1085 *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain 1086 code:status 1087 userInfo:nil]; 1088 } 1089 return status == noErr; 1090} 1091 1092#endif // ! TARGET_IPHONE_SIMULATOR 1093 1094@end 1095 1096#endif // TARGET_OS_IPHONE 1097 1098#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES