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