diff options
Diffstat (limited to 'example/common/gtm-oauth2/Source/Mac')
3 files changed, 1168 insertions, 0 deletions
diff --git a/example/common/gtm-oauth2/Source/Mac/GTMOAuth2Window.xib b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2Window.xib new file mode 100644 index 00000000..befc2123 --- /dev/null +++ b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2Window.xib @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="4457.6" systemVersion="12E55" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none"> + <dependencies> + <deployment version="1050" defaultVersion="1080" identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="4457.6"/> + <plugIn identifier="com.apple.WebKitIBPlugin" version="3330"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="GTMOAuth2WindowController"> + <connections> + <action selector="closeWindow:" destination="17" id="42"/> + <outlet property="keychainCheckbox" destination="43" id="46"/> + <outlet property="webBackButton" destination="19" id="47"/> + <outlet property="webCloseButton" destination="17" id="48"/> + <outlet property="webView" destination="5" id="49"/> + <outlet property="window" destination="3" id="8"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application"/> + <window title="Sign In" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" wantsToBeColor="NO" visibleAtLaunch="NO" animationBehavior="default" id="3"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES"/> + <windowPositionMask key="initialPositionMask" leftStrut="YES" bottomStrut="YES"/> + <rect key="contentRect" x="74" y="707" width="515" height="419"/> + <rect key="screenRect" x="0.0" y="0.0" width="1920" height="1178"/> + <value key="minSize" type="size" width="475" height="290"/> + <view key="contentView" id="4"> + <rect key="frame" x="0.0" y="0.0" width="515" height="419"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <webView id="5"> + <rect key="frame" x="0.0" y="20" width="515" height="399"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <webPreferences key="preferences" defaultFontSize="12" defaultFixedFontSize="12"/> + <connections> + <action selector="goBack:" destination="19" id="28"/> + <action selector="goForward:" destination="26" id="29"/> + </connections> + </webView> + <button id="17"> + <rect key="frame" x="479" y="0.0" width="16" height="19"/> + <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> + <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSStopProgressTemplate" imagePosition="overlaps" alignment="center" imageScaling="proportionallyDown" inset="2" id="18"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + <string key="keyEquivalent" base64-UTF8="YES"> +Gw +</string> + </buttonCell> + </button> + <button id="19"> + <rect key="frame" x="437" y="0.0" width="16" height="19"/> + <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> + <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSGoLeftTemplate" imagePosition="overlaps" alignment="center" imageScaling="proportionallyDown" inset="2" id="20"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <connections> + <binding destination="-2" name="enabled" keyPath="webView.canGoBack" id="31"/> + </connections> + </button> + <button id="26"> + <rect key="frame" x="456" y="0.0" width="16" height="19"/> + <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> + <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSGoRightTemplate" imagePosition="only" alignment="center" imageScaling="proportionallyDown" inset="2" id="27"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <connections> + <binding destination="-2" name="enabled" keyPath="webView.canGoForward" id="35"/> + </connections> + </button> + <button id="43"> + <rect key="frame" x="2" y="1" width="429" height="18"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <buttonCell key="cell" type="check" title="Stay signed in to this account" bezelStyle="regularSquare" imagePosition="left" alignment="left" controlSize="small" state="on" inset="2" id="44"> + <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> + <font key="font" metaFont="smallSystem"/> + </buttonCell> + </button> + </subviews> + </view> + <connections> + <outlet property="delegate" destination="-2" id="7"/> + </connections> + </window> + <userDefaultsController id="32"/> + </objects> + <resources> + <image name="NSGoLeftTemplate" width="9" height="9"/> + <image name="NSGoRightTemplate" width="9" height="9"/> + <image name="NSStopProgressTemplate" width="11" height="11"/> + </resources> + <classes> + <class className="GTMOAuth2WindowController" superclassName="NSWindowController"> + <source key="sourceIdentifier" type="project" relativePath="./Classes/GTMOAuth2WindowController.h"/> + <relationships> + <relationship kind="action" name="closeWindow:"/> + <relationship kind="outlet" name="completionPlaceholder_"/> + <relationship kind="outlet" name="delegate_"/> + <relationship kind="outlet" name="keychainCheckbox" candidateClass="NSButton"/> + <relationship kind="outlet" name="userData_"/> + <relationship kind="outlet" name="webBackButton" candidateClass="NSButton"/> + <relationship kind="outlet" name="webCloseButton" candidateClass="NSButton"/> + <relationship kind="outlet" name="webView" candidateClass="WebView"/> + </relationships> + </class> + </classes> +</document>
\ No newline at end of file diff --git a/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.h b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.h new file mode 100644 index 00000000..9ff89b70 --- /dev/null +++ b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.h @@ -0,0 +1,332 @@ +/* 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. + */ + +// GTMOAuth2WindowController +// +// This window controller for Mac handles sign-in via OAuth2 to Google or +// other services. +// +// This controller is not reusable; create a new instance of this controller +// every time the user will sign in. +// +// Sample usage for signing in to a Google service: +// +// static NSString *const kKeychainItemName = @”My App: Google Plus”; +// NSString *scope = @"https://www.googleapis.com/auth/plus.me"; +// +// +// GTMOAuth2WindowController *windowController; +// windowController = [[[GTMOAuth2WindowController alloc] initWithScope:scope +// clientID:clientID +// clientSecret:clientSecret +// keychainItemName:kKeychainItemName +// resourceBundle:nil] autorelease]; +// +// [windowController signInSheetModalForWindow:mMainWindow +// delegate:self +// finishedSelector:@selector(windowController:finishedWithAuth:error:)]; +// +// The finished selector should have a signature matching this: +// +// - (void)windowController:(GTMOAuth2WindowController *)windowController +// finishedWithAuth:(GTMOAuth2Authentication *)auth +// error:(NSError *)error { +// if (error != nil) { +// // sign in failed +// } else { +// // sign in succeeded +// // +// // with the GTL library, pass the authentication to the service object, +// // like +// // [[self contactService] setAuthorizer:auth]; +// // +// // or use it to sign a request directly, like +// // BOOL isAuthorizing = [self authorizeRequest:request +// // delegate:self +// // didFinishSelector:@selector(auth:finishedWithError:)]; +// } +// } +// +// To sign in to services other than Google, use the longer init method, +// as shown in the sample application +// +// If the network connection is lost for more than 30 seconds while the sign-in +// html is displayed, the notification kGTLOAuthNetworkLost will be sent. + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +#include <Foundation/Foundation.h> + +#if !TARGET_OS_IPHONE + +#import <Cocoa/Cocoa.h> +#import <WebKit/WebKit.h> + +// GTMHTTPFetcher.h brings in GTLDefines/GDataDefines +#import "GTMHTTPFetcher.h" + +#import "GTMOAuth2SignIn.h" +#import "GTMOAuth2Authentication.h" +#import "GTMHTTPFetchHistory.h" // for GTMCookieStorage + +@class GTMOAuth2SignIn; + +@interface GTMOAuth2WindowController : NSWindowController { + @private + // IBOutlets + NSButton *keychainCheckbox_; + WebView *webView_; + NSButton *webCloseButton_; + NSButton *webBackButton_; + + // the object responsible for the sign-in networking sequence; it holds + // onto the authentication object as well + GTMOAuth2SignIn *signIn_; + + // the page request to load when awakeFromNib occurs + NSURLRequest *initialRequest_; + + // local storage for WebKit cookies so they're not shared with Safari + GTMCookieStorage *cookieStorage_; + + // the user we're calling back + // + // the delegate is retained only until the callback is invoked + // or the sign-in is canceled + id delegate_; + SEL finishedSelector_; + +#if NS_BLOCKS_AVAILABLE + void (^completionBlock_)(GTMOAuth2Authentication *, NSError *); +#elif !__LP64__ + // placeholders: for 32-bit builds, keep the size of the object's ivar section + // the same with and without blocks +#ifndef __clang_analyzer__ + id completionPlaceholder_; +#endif +#endif + + // flag allowing application to quit during display of sign-in sheet on 10.6 + // and later + BOOL shouldAllowApplicationTermination_; + + // delegate method for handling URLs to be opened in external windows + SEL externalRequestSelector_; + + BOOL isWindowShown_; + + // paranoid flag to ensure we only close once during the sign-in sequence + BOOL hasDoneFinalRedirect_; + + // paranoid flag to ensure we only call the user back once + BOOL hasCalledFinished_; + + // if non-nil, we display as a sheet on the specified window + NSWindow *sheetModalForWindow_; + + // if non-empty, the name of the application and service used for the + // keychain item + NSString *keychainItemName_; + + // if non-nil, the html string to be displayed immediately upon opening + // of the web view + NSString *initialHTMLString_; + + // if true, we allow default WebView handling of cookies, so the + // same user remains signed in each time the dialog is displayed + BOOL shouldPersistUser_; + + // user-defined data + id userData_; + NSMutableDictionary *properties_; +} + +// User interface elements +@property (nonatomic, assign) IBOutlet NSButton *keychainCheckbox; +@property (nonatomic, assign) IBOutlet WebView *webView; +@property (nonatomic, assign) IBOutlet NSButton *webCloseButton; +@property (nonatomic, assign) IBOutlet NSButton *webBackButton; + +// The application and service name to use for saving the auth tokens +// to the keychain +@property (nonatomic, copy) NSString *keychainItemName; + +// If true, the sign-in will remember which user was last signed in +// +// Defaults to false, so showing the sign-in window will always ask for +// the username and password, rather than skip to the grant authorization +// page. During development, it may be convenient to set this to true +// to speed up signing in. +@property (nonatomic, assign) BOOL shouldPersistUser; + +// Optional html string displayed immediately upon opening the web view +// +// This string is visible just until the sign-in web page loads, and +// may be used for a "Loading..." type of message +@property (nonatomic, copy) NSString *initialHTMLString; + +// The default timeout for an unreachable network during display of the +// sign-in page is 30 seconds, after which the notification +// kGTLOAuthNetworkLost is sent; set this to 0 to have no timeout +@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval; + +// On 10.6 and later, the sheet can allow application termination by calling +// NSWindow's setPreventsApplicationTerminationWhenModal: +@property (nonatomic, assign) BOOL shouldAllowApplicationTermination; + +// Selector for a delegate method to handle requests sent to an external +// browser. +// +// Selector should have a signature matching +// - (void)windowController:(GTMOAuth2WindowController *)controller +// opensRequest:(NSURLRequest *)request; +// +// The controller's default behavior is to use NSWorkspace's openURL: +@property (nonatomic, assign) SEL externalRequestSelector; + +// The underlying object to hold authentication tokens and authorize http +// requests +@property (nonatomic, retain, readonly) GTMOAuth2Authentication *authentication; + +// The underlying object which performs the sign-in networking sequence +@property (nonatomic, retain, readonly) GTMOAuth2SignIn *signIn; + +// Any arbitrary data object the user would like the controller to retain +@property (nonatomic, retain) id userData; + +// Stored property values are retained for the convenience of the caller +- (void)setProperty:(id)obj forKey:(NSString *)key; +- (id)propertyForKey:(NSString *)key; + +@property (nonatomic, retain) NSDictionary *properties; + +- (IBAction)closeWindow:(id)sender; + +// Create a controller for authenticating to Google services +// +// scope is the requested scope of authorization +// (like "http://www.google.com/m8/feeds") +// +// keychainItemName is used for storing the token on the keychain, +// and is required for the "remember for later" checkbox to be shown; +// keychainItemName should be like "My Application: Google Contacts" +// (or set to nil if no persistent keychain storage is desired) +// +// resourceBundle may be nil if the window is in the main bundle's nib +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (id)controllerWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName // may be nil + resourceBundle:(NSBundle *)bundle; // may be nil + +- (id)initWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle; +#endif + +// Create a controller for authenticating to non-Google services, taking +// explicit endpoint URLs and an authentication object ++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName // may be nil + resourceBundle:(NSBundle *)bundle; // may be nil + +// This is the designated initializer +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle; + +// Entry point to begin displaying the sign-in window +// +// the finished selector should have a signature matching +// - (void)windowController:(GTMOAuth2WindowController *)windowController +// finishedWithAuth:(GTMOAuth2Authentication *)auth +// error:(NSError *)error { +// +// Once the finished method has been invoked with no error, the auth object +// may be used to authorize requests (refreshing the access token, if necessary, +// and adding the auth header) like: +// +// [authorizer authorizeRequest:myNSMutableURLRequest] +// delegate:self +// didFinishSelector:@selector(auth:finishedWithError:)]; +// +// or can be stored in a GTL service object like +// GTLServiceGoogleContact *service = [self contactService]; +// [service setAuthorizer:auth]; +// +// The delegate is retained only until the finished selector is invoked or +// the sign-in is canceled +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector; + +#if NS_BLOCKS_AVAILABLE +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + completionHandler:(void (^)(GTMOAuth2Authentication *auth, NSError *error))handler; +#endif + +- (void)cancelSigningIn; + +// Subclasses may override authNibName to specify a custom name ++ (NSString *)authNibName; + +// apps may replace the sign-in class with their own subclass of it ++ (Class)signInClass; ++ (void)setSignInClass:(Class)theClass; + +// Revocation of an authorized token from Google +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth; +#endif + +// Keychain +// +// The keychain checkbox is shown if the keychain application service +// name (typically set in the initWithScope: method) is non-empty +// + +// Create an authentication object for Google services from the access +// token and secret stored in the keychain; if no token is available, return +// an unauthorized auth object +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret; +#endif + +// Add tokens from the keychain, if available, to the authentication object +// +// returns YES if the authentication object was authorized from the keychain ++ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)auth; + +// Method for deleting the stored access token and secret, useful for "signing +// out" ++ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName; + +// Method for saving the stored access token and secret; typically, this method +// is used only by the window controller ++ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)auth; +@end + +#endif // #if !TARGET_OS_IPHONE + +#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.m b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.m new file mode 100644 index 00000000..948975bf --- /dev/null +++ b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.m @@ -0,0 +1,727 @@ +/* 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. + */ + +#import <Foundation/Foundation.h> + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +#if !TARGET_OS_IPHONE + +#import "GTMOAuth2WindowController.h" + +@interface GTMOAuth2WindowController () +@property (nonatomic, retain) GTMOAuth2SignIn *signIn; +@property (nonatomic, copy) NSURLRequest *initialRequest; +@property (nonatomic, retain) GTMCookieStorage *cookieStorage; +@property (nonatomic, retain) NSWindow *sheetModalForWindow; + +- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil; +- (void)setupSheetTerminationHandling; +- (void)destroyWindow; +- (void)handlePrematureWindowClose; +- (BOOL)shouldUseKeychain; +- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request; +- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error; +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo; + +- (void)handleCookiesForResponse:(NSURLResponse *)response; +- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request; +@end + +const char *kKeychainAccountName = "OAuth"; + +@implementation GTMOAuth2WindowController + +// IBOutlets +@synthesize keychainCheckbox = keychainCheckbox_, + webView = webView_, + webCloseButton = webCloseButton_, + webBackButton = webBackButton_; + +// regular ivars +@synthesize signIn = signIn_, + initialRequest = initialRequest_, + cookieStorage = cookieStorage_, + sheetModalForWindow = sheetModalForWindow_, + keychainItemName = keychainItemName_, + initialHTMLString = initialHTMLString_, + shouldAllowApplicationTermination = shouldAllowApplicationTermination_, + externalRequestSelector = externalRequestSelector_, + shouldPersistUser = shouldPersistUser_, + userData = userData_, + properties = properties_; + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +// Create a controller for authenticating to Google services ++ (id)controllerWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + return [[[self alloc] initWithScope:scope + clientID:clientID + clientSecret:clientSecret + keychainItemName:keychainItemName + resourceBundle:bundle] autorelease]; +} + +- (id)initWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + 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 + resourceBundle:bundle]; +} +#endif + +// Create a controller for authenticating to any service ++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + return [[[self alloc] initWithAuthentication:auth + authorizationURL:authorizationURL + keychainItemName:keychainItemName + resourceBundle:bundle] autorelease]; +} + +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + if (bundle == nil) { + bundle = [NSBundle mainBundle]; + } + + NSString *nibName = [[self class] authNibName]; + NSString *nibPath = [bundle pathForResource:nibName + ofType:@"nib"]; + self = [super initWithWindowNibPath:nibPath + owner:self]; + if (self != nil) { + // use the supplied auth and OAuth endpoint URLs + Class signInClass = [[self class] signInClass]; + signIn_ = [[signInClass alloc] initWithAuthentication:auth + authorizationURL:authorizationURL + delegate:self + webRequestSelector:@selector(signIn:displayRequest:) + finishedSelector:@selector(signIn:finishedWithAuth:error:)]; + keychainItemName_ = [keychainItemName copy]; + + // create local, temporary storage for WebKit cookies + cookieStorage_ = [[GTMCookieStorage alloc] init]; + } + return self; +} + +- (void)dealloc { + [signIn_ release]; + [initialRequest_ release]; + [cookieStorage_ release]; + [delegate_ release]; +#if NS_BLOCKS_AVAILABLE + [completionBlock_ release]; +#endif + [sheetModalForWindow_ release]; + [keychainItemName_ release]; + [initialHTMLString_ release]; + [userData_ release]; + [properties_ release]; + + [super dealloc]; +} + +- (void)awakeFromNib { + // load the requested initial sign-in page + [self.webView setResourceLoadDelegate:self]; + [self.webView setPolicyDelegate:self]; + + // the app may prefer some html other than blank white to be displayed + // before the sign-in web page loads + NSString *html = self.initialHTMLString; + if ([html length] > 0) { + [[self.webView mainFrame] loadHTMLString:html baseURL:nil]; + } + + // hide the keychain checkbox if we're not supporting keychain + BOOL hideKeychainCheckbox = ![self shouldUseKeychain]; + + const NSTimeInterval kJanuary2011 = 1293840000; + BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011); + if (isDateValid) { + // start the asynchronous load of the sign-in web page + [[self.webView mainFrame] performSelector:@selector(loadRequest:) + withObject:self.initialRequest + afterDelay:0.01 + inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; + } 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 let them make a useful screenshot to show + // 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 htmlTemplate = @"<html><body><div align=center><font size='7'>" + @"⌚ ?<br><i>System Clock Incorrect</i><br>%@" + @"</font></div></body></html>"; + NSString *errHTML = [NSString stringWithFormat:htmlTemplate, [NSDate date]]; + + [[webView_ mainFrame] loadHTMLString:errHTML baseURL:nil]; + hideKeychainCheckbox = YES; + } + +#if DEBUG + // Verify that Javascript is enabled + BOOL hasJS = [[webView_ preferences] isJavaScriptEnabled]; + NSAssert(hasJS, @"GTMOAuth2: Javascript is required"); +#endif + + [keychainCheckbox_ setHidden:hideKeychainCheckbox]; +} + ++ (NSString *)authNibName { + // subclasses may override this to specify a custom nib name + return @"GTMOAuth2Window"; +} + +#pragma mark - + +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector { + // check the selector on debug builds + GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector, + @encode(GTMOAuth2WindowController *), @encode(GTMOAuth2Authentication *), + @encode(NSError *), 0); + + delegate_ = [delegate retain]; + finishedSelector_ = finishedSelector; + + [self signInCommonForWindow:parentWindowOrNil]; +} + +#if NS_BLOCKS_AVAILABLE +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + completionHandler:(void (^)(GTMOAuth2Authentication *, NSError *))handler { + completionBlock_ = [handler copy]; + + [self signInCommonForWindow:parentWindowOrNil]; +} +#endif + +- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil { + self.sheetModalForWindow = parentWindowOrNil; + hasDoneFinalRedirect_ = NO; + hasCalledFinished_ = NO; + + [self.signIn startSigningIn]; +} + +- (void)cancelSigningIn { + // The user 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 signIn object's cancel method will close the window + [self.signIn cancelSigningIn]; + hasDoneFinalRedirect_ = YES; +} + +- (IBAction)closeWindow:(id)sender { + // dismiss the window/sheet before we call back the client + [self destroyWindow]; + [self handlePrematureWindowClose]; +} + +#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 close the window + // + // All web requests and all window closing goes through this routine + +#if DEBUG + if ((isWindowShown_ && request != nil) + || (!isWindowShown_ && request == nil)) { + NSLog(@"Window state unexpected for request %@", [request URL]); + return; + } +#endif + + if (request != nil) { + // display the request + self.initialRequest = request; + + NSWindow *parentWindow = self.sheetModalForWindow; + if (parentWindow) { + [self setupSheetTerminationHandling]; + + NSWindow *sheet = [self window]; + [NSApp beginSheet:sheet + modalForWindow:parentWindow + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:nil]; + } else { + // modeless + [self showWindow:self]; + } + isWindowShown_ = YES; + } else { + // request was nil + [self destroyWindow]; + } +} + +- (void)setupSheetTerminationHandling { + NSWindow *sheet = [self window]; + + SEL sel = @selector(setPreventsApplicationTerminationWhenModal:); + if ([sheet respondsToSelector:sel]) { + // setPreventsApplicationTerminationWhenModal is available in NSWindow + // on 10.6 and later + BOOL boolVal = !self.shouldAllowApplicationTermination; + NSMethodSignature *sig = [sheet methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:sheet]; + [invocation setArgument:&boolVal atIndex:2]; + [invocation invoke]; + } +} + +- (void)destroyWindow { + // no request; close the window + + // Avoid more callbacks after the close happens, as the window + // controller may be gone. + [self.webView stopLoading:nil]; + + NSWindow *parentWindow = self.sheetModalForWindow; + if (parentWindow) { + [NSApp endSheet:[self window]]; + } else { + // defer closing the window, in case we're responding to some window event + [[self window] performSelector:@selector(close) + withObject:nil + afterDelay:0.1 + inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; + + } + isWindowShown_ = NO; +} + +- (void)handlePrematureWindowClose { + if (!hasDoneFinalRedirect_) { + // tell the sign-in object to tell the user's finished method + // that we're done + [self.signIn windowWasClosed]; + hasDoneFinalRedirect_ = YES; + } +} + +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { + [sheet orderOut:self]; + + self.sheetModalForWindow = nil; +} + +- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error { + if (!hasCalledFinished_) { + hasCalledFinished_ = YES; + + if (error == nil) { + BOOL shouldUseKeychain = [self shouldUseKeychain]; + if (shouldUseKeychain) { + BOOL canAuthorize = auth.canAuthorize; + BOOL isKeychainChecked = ([keychainCheckbox_ state] == NSOnState); + + NSString *keychainItemName = self.keychainItemName; + + if (isKeychainChecked && canAuthorize) { + // save the auth params in the keychain + [[self class] saveAuthToKeychainForName:keychainItemName + 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_(auth, error); + + // release the block here to avoid a retain loop on the controller + [completionBlock_ autorelease]; + completionBlock_ = nil; + } +#endif + } +} + +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 WebView methods + +- (NSURLRequest *)webView:(WebView *)sender resource:(id)identifier willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse fromDataSource:(WebDataSource *)dataSource { + // override WebKit's cookie storage with our own to avoid cookie persistence + // across sign-ins and interaction with the Safari browser's sign-in state + [self handleCookiesForResponse:redirectResponse]; + request = [self addCookiesToRequest:request]; + + if (!hasDoneFinalRedirect_) { + hasDoneFinalRedirect_ = [self.signIn requestRedirectedToRequest:request]; + if (hasDoneFinalRedirect_) { + // signIn has told the window to close + return nil; + } + } + return request; +} + +- (void)webView:(WebView *)sender resource:(id)identifier didReceiveResponse:(NSURLResponse *)response fromDataSource:(WebDataSource *)dataSource { + // override WebKit's cookie storage with our own + [self handleCookiesForResponse:response]; +} + +- (void)webView:(WebView *)sender resource:(id)identifier didFinishLoadingFromDataSource:(WebDataSource *)dataSource { + NSString *title = [sender stringByEvaluatingJavaScriptFromString:@"document.title"]; + if ([title length] > 0) { + [self.signIn titleChanged:title]; + } + + [signIn_ cookiesChanged:(NSHTTPCookieStorage *)cookieStorage_]; +} + +- (void)webView:(WebView *)sender resource:(id)identifier didFailLoadingWithError:(NSError *)error fromDataSource:(WebDataSource *)dataSource { + [self.signIn loadFailedWithError:error]; +} + +- (void)windowWillClose:(NSNotification *)note { + if (isWindowShown_) { + [self handlePrematureWindowClose]; + } + isWindowShown_ = NO; +} + +- (void)webView:(WebView *)webView +decidePolicyForNewWindowAction:(NSDictionary *)actionInformation + request:(NSURLRequest *)request + newFrameName:(NSString *)frameName +decisionListener:(id<WebPolicyDecisionListener>)listener { + SEL sel = self.externalRequestSelector; + if (sel) { + [delegate_ performSelector:sel + withObject:self + withObject:request]; + } else { + // default behavior is to open the URL in NSWorkspace's default browser + NSURL *url = [request URL]; + [[NSWorkspace sharedWorkspace] openURL:url]; + } + [listener ignore]; +} + +#pragma mark Cookie management + +// Rather than let the WebView use Safari's default cookie storage, we intercept +// requests and response to segregate and later discard cookies from signing in. +// +// This allows the application to actually sign out by discarding the auth token +// rather than the user being kept signed in by the cookies. + +- (void)handleCookiesForResponse:(NSURLResponse *)response { + if (self.shouldPersistUser) { + // we'll let WebKit handle the cookies; they'll persist across apps + // and across runs of this app + return; + } + + if ([response respondsToSelector:@selector(allHeaderFields)]) { + // grab the cookies from the header as NSHTTPCookies and store them locally + NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields]; + if (headers) { + NSURL *url = [response URL]; + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers + forURL:url]; + if ([cookies count] > 0) { + [cookieStorage_ setCookies:cookies]; + } + } + } +} + +- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request { + if (self.shouldPersistUser) { + // we'll let WebKit handle the cookies; they'll persist across apps + // and across runs of this app + return request; + } + + // override WebKit's usual automatic storage of cookies + NSMutableURLRequest *mutableRequest = [[request mutableCopy] autorelease]; + [mutableRequest setHTTPShouldHandleCookies:NO]; + + // add our locally-stored cookies for this URL, if any + NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]]; + if ([cookies count] > 0) { + NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + NSString *cookieHeader = [headers objectForKey:@"Cookie"]; + if (cookieHeader) { + [mutableRequest setValue:cookieHeader forHTTPHeaderField:@"Cookie"]; + } + } + return mutableRequest; +} + +#pragma mark Keychain support + ++ (NSString *)prefsKeyForName:(NSString *)keychainItemName { + NSString *result = [@"OAuth2: " stringByAppendingString:keychainItemName]; + return result; +} + ++ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)auth { + + [self removeAuthFromKeychainForName:keychainItemName]; + + // don't save unless we have a token that can really authorize requests + if (!auth.canAuthorize) return NO; + + // make a response string containing the values we want to save + NSString *password = [auth persistenceResponseString]; + + SecKeychainRef defaultKeychain = NULL; + SecKeychainItemRef *dontWantItemRef= NULL; + const char *utf8ServiceName = [keychainItemName UTF8String]; + const char *utf8Password = [password UTF8String]; + + OSStatus err = SecKeychainAddGenericPassword(defaultKeychain, + (UInt32) strlen(utf8ServiceName), utf8ServiceName, + (UInt32) strlen(kKeychainAccountName), kKeychainAccountName, + (UInt32) strlen(utf8Password), utf8Password, + dontWantItemRef); + BOOL didSucceed = (err == noErr); + if (didSucceed) { + // write to preferences that we have a keychain item (so we know later + // that we can read from the keychain without raising a permissions dialog) + NSString *prefKey = [self prefsKeyForName:keychainItemName]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:YES forKey:prefKey]; + } + + return didSucceed; +} + ++ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName { + + SecKeychainRef defaultKeychain = NULL; + SecKeychainItemRef itemRef = NULL; + const char *utf8ServiceName = [keychainItemName UTF8String]; + + // we don't really care about the password here, we just want to + // get the SecKeychainItemRef so we can delete it. + OSStatus err = SecKeychainFindGenericPassword (defaultKeychain, + (UInt32) strlen(utf8ServiceName), utf8ServiceName, + (UInt32) strlen(kKeychainAccountName), kKeychainAccountName, + 0, NULL, // ignore password + &itemRef); + if (err != noErr) { + // failure to find is success + return YES; + } else { + // found something, so delete it + err = SecKeychainItemDelete(itemRef); + CFRelease(itemRef); + + // remove our preference key + NSString *prefKey = [self prefsKeyForName:keychainItemName]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults removeObjectForKey:prefKey]; + + return (err == noErr); + } +} + +#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]; + + [GTMOAuth2WindowController authorizeFromKeychainForName:keychainItemName + authentication:auth]; + return auth; +} +#endif + ++ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)newAuth { + [newAuth setAccessToken:nil]; + + // before accessing the keychain, check preferences to verify that we've + // previously saved a token to the keychain (so we don't needlessly raise + // a keychain access permission dialog) + NSString *prefKey = [self prefsKeyForName:keychainItemName]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + BOOL flag = [defaults boolForKey:prefKey]; + if (!flag) { + return NO; + } + + BOOL didGetTokens = NO; + + SecKeychainRef defaultKeychain = NULL; + const char *utf8ServiceName = [keychainItemName UTF8String]; + SecKeychainItemRef *dontWantItemRef = NULL; + + void *passwordBuff = NULL; + UInt32 passwordBuffLength = 0; + + OSStatus err = SecKeychainFindGenericPassword(defaultKeychain, + (UInt32) strlen(utf8ServiceName), utf8ServiceName, + (UInt32) strlen(kKeychainAccountName), kKeychainAccountName, + &passwordBuffLength, &passwordBuff, + dontWantItemRef); + if (err == noErr && passwordBuff != NULL) { + + NSString *password = [[[NSString alloc] initWithBytes:passwordBuff + length:passwordBuffLength + encoding:NSUTF8StringEncoding] autorelease]; + + // free the password buffer that was allocated above + SecKeychainItemFreeContent(NULL, passwordBuff); + + if (password != nil) { + [newAuth setKeysForResponseString:password]; + didGetTokens = YES; + } + } + return didGetTokens; +} + +#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 Accessors + +- (GTMOAuth2Authentication *)authentication { + return self.signIn.authentication; +} + +- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val { + self.signIn.networkLossTimeoutInterval = val; +} + +- (NSTimeInterval)networkLossTimeoutInterval { + return self.signIn.networkLossTimeoutInterval; +} + +- (BOOL)shouldUseKeychain { + NSString *name = self.keychainItemName; + return ([name length] > 0); +} + +@end + +#endif // #if !TARGET_OS_IPHONE + +#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES |