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