PageRenderTime 218ms CodeModel.GetById 18ms app.highlight 191ms RepoModel.GetById 1ms app.codeStats 0ms

/core/externals/update-engine/externals/gdata-objectivec-client/Source/OAuth2/Touch/GTMOAuth2ViewControllerTouch.m

http://macfuse.googlecode.com/
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        @"&#x231A; ?<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