1

Archive Project

This commit is contained in:
Kim Wittenburg
2017-07-25 17:21:13 +02:00
parent 7d4f001502
commit 15062dfb27
66 changed files with 14172 additions and 4692 deletions

187
OAuth 2/GTMHTTPFetchHistory.h Executable file
View File

@@ -0,0 +1,187 @@
/* 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.
*/
//
// GTMHTTPFetchHistory.h
//
//
// Users of the GTMHTTPFetcher class may optionally create and set a fetch
// history object. The fetch history provides "memory" between subsequent
// fetches, including:
//
// - For fetch responses with Etag headers, the fetch history
// remembers the response headers. Future fetcher requests to the same URL
// will be given an "If-None-Match" header, telling the server to return
// a 304 Not Modified status if the response is unchanged, reducing the
// server load and network traffic.
//
// - Optionally, the fetch history can cache the ETagged data that was returned
// in the responses that contained Etag headers. If a later fetch
// results in a 304 status, the fetcher will return the cached ETagged data
// to the client along with a 200 status, hiding the 304.
//
// - The fetch history can track cookies.
//
#pragma once
#import <Foundation/Foundation.h>
#import "GTMHTTPFetcher.h"
#undef _EXTERN
#undef _INITIALIZE_AS
#ifdef GTMHTTPFETCHHISTORY_DEFINE_GLOBALS
#define _EXTERN
#define _INITIALIZE_AS(x) =x
#else
#if defined(__cplusplus)
#define _EXTERN extern "C"
#else
#define _EXTERN extern
#endif
#define _INITIALIZE_AS(x)
#endif
// default data cache size for when we're caching responses to handle "not
// modified" errors for the client
#if GTM_IPHONE
// iPhone: up to 1MB memory
_EXTERN const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity _INITIALIZE_AS(1*1024*1024);
#else
// Mac OS X: up to 15MB memory
_EXTERN const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity _INITIALIZE_AS(15*1024*1024);
#endif
// forward declarations
@class GTMURLCache;
@class GTMCookieStorage;
@interface GTMHTTPFetchHistory : NSObject <GTMHTTPFetchHistoryProtocol> {
@private
GTMURLCache *etaggedDataCache_;
BOOL shouldRememberETags_;
BOOL shouldCacheETaggedData_; // if NO, then only headers are cached
GTMCookieStorage *cookieStorage_;
}
// With caching enabled, previously-cached data will be returned instead of
// 304 Not Modified responses when repeating a fetch of an URL that previously
// included an ETag header in its response
@property (assign) BOOL shouldRememberETags; // default: NO
@property (assign) BOOL shouldCacheETaggedData; // default: NO
// the default ETag data cache capacity is kGTMDefaultETaggedDataCacheMemoryCapacity
@property (assign) NSUInteger memoryCapacity;
@property (retain) GTMCookieStorage *cookieStorage;
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes
shouldCacheETaggedData:(BOOL)shouldCacheETaggedData;
- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet;
- (void)clearETaggedDataCache;
- (void)clearHistory;
- (void)removeAllCookies;
@end
// GTMURLCache and GTMCachedURLResponse have interfaces similar to their
// NSURLCache counterparts, in hopes that someday the NSURLCache versions
// can be used. But in 10.5.8, those are not reliable enough except when
// used with +setSharedURLCache. Our goal here is just to cache
// responses for handling If-None-Match requests that return
// "Not Modified" responses, not for replacing the general URL
// caches.
@interface GTMCachedURLResponse : NSObject {
@private
NSURLResponse *response_;
NSData *data_;
NSDate *useDate_; // date this response was last saved or used
NSDate *reservationDate_; // date this response's ETag was used
}
@property (readonly) NSURLResponse* response;
@property (readonly) NSData* data;
// date the response was saved or last accessed
@property (retain) NSDate *useDate;
// date the response's ETag header was last used for a fetch request
@property (retain) NSDate *reservationDate;
- (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data;
@end
@interface GTMURLCache : NSObject {
NSMutableDictionary *responses_; // maps request URL to GTMCachedURLResponse
NSUInteger memoryCapacity_; // capacity of NSDatas in the responses
NSUInteger totalDataSize_; // sum of sizes of NSDatas of all responses
NSTimeInterval reservationInterval_; // reservation expiration interval
}
@property (assign) NSUInteger memoryCapacity;
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes;
- (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request;
- (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
- (void)removeCachedResponseForRequest:(NSURLRequest *)request;
- (void)removeAllCachedResponses;
// for unit testing
- (void)setReservationInterval:(NSTimeInterval)secs;
- (NSDictionary *)responses;
- (NSUInteger)totalDataSize;
@end
@interface GTMCookieStorage : NSObject <GTMCookieStorageProtocol> {
@private
// The cookie storage object manages an array holding cookies, but the array
// is allocated externally (it may be in a fetcher object or the static
// fetcher cookie array.) See the fetcher's setCookieStorageMethod:
// for allocation of this object and assignment of its cookies array.
NSMutableArray *cookies_;
}
// add all NSHTTPCookies in the supplied array to the storage array,
// replacing cookies in the storage array as appropriate
// Side effect: removes expired cookies from the storage array
- (void)setCookies:(NSArray *)newCookies;
// retrieve all cookies appropriate for the given URL, considering
// domain, path, cookie name, expiration, security setting.
// Side effect: removes expired cookies from the storage array
- (NSArray *)cookiesForURL:(NSURL *)theURL;
// return a cookie with the same name, domain, and path as the
// given cookie, or else return nil if none found
//
// Both the cookie being tested and all stored cookies should
// be valid (non-nil name, domains, paths)
- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie;
// remove any expired cookies, excluding cookies with nil expirations
- (void)removeExpiredCookies;
- (void)removeAllCookies;
@end

590
OAuth 2/GTMHTTPFetchHistory.m Executable file
View File

@@ -0,0 +1,590 @@
/* Copyright (c) 2010 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.
*/
//
// GTMHTTPFetchHistory.m
//
#define GTMHTTPFETCHHISTORY_DEFINE_GLOBALS 1
#import "GTMHTTPFetchHistory.h"
const NSTimeInterval kCachedURLReservationInterval = 60.0; // 1 minute
static NSString* const kGTMIfNoneMatchHeader = @"If-None-Match";
static NSString* const kGTMETagHeader = @"Etag";
@implementation GTMCookieStorage
- (id)init {
self = [super init];
if (self != nil) {
cookies_ = [[NSMutableArray alloc] init];
}
return self;
}
- (void)dealloc {
[cookies_ release];
[super dealloc];
}
// add all cookies in the new cookie array to the storage,
// replacing stored cookies as appropriate
//
// Side effect: removes expired cookies from the storage array
- (void)setCookies:(NSArray *)newCookies {
@synchronized(cookies_) {
[self removeExpiredCookies];
for (NSHTTPCookie *newCookie in newCookies) {
if ([[newCookie name] length] > 0
&& [[newCookie domain] length] > 0
&& [[newCookie path] length] > 0) {
// remove the cookie if it's currently in the array
NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie];
if (oldCookie) {
[cookies_ removeObjectIdenticalTo:oldCookie];
}
// make sure the cookie hasn't already expired
NSDate *expiresDate = [newCookie expiresDate];
if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) {
[cookies_ addObject:newCookie];
}
} else {
NSAssert1(NO, @"Cookie incomplete: %@", newCookie);
}
}
}
}
- (void)deleteCookie:(NSHTTPCookie *)cookie {
@synchronized(cookies_) {
NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie];
if (foundCookie) {
[cookies_ removeObjectIdenticalTo:foundCookie];
}
}
}
// retrieve all cookies appropriate for the given URL, considering
// domain, path, cookie name, expiration, security setting.
// Side effect: removed expired cookies from the storage array
- (NSArray *)cookiesForURL:(NSURL *)theURL {
NSMutableArray *foundCookies = nil;
@synchronized(cookies_) {
[self removeExpiredCookies];
// we'll prepend "." to the desired domain, since we want the
// actual domain "nytimes.com" to still match the cookie domain
// ".nytimes.com" when we check it below with hasSuffix
NSString *host = [[theURL host] lowercaseString];
NSString *path = [theURL path];
NSString *scheme = [theURL scheme];
NSString *domain = nil;
BOOL isLocalhostRetrieval = NO;
if ([host isEqual:@"localhost"]) {
isLocalhostRetrieval = YES;
} else {
if (host) {
domain = [@"." stringByAppendingString:host];
}
}
NSUInteger numberOfCookies = [cookies_ count];
for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
NSString *cookieDomain = [[storedCookie domain] lowercaseString];
NSString *cookiePath = [storedCookie path];
BOOL cookieIsSecure = [storedCookie isSecure];
BOOL isDomainOK;
if (isLocalhostRetrieval) {
// prior to 10.5.6, the domain stored into NSHTTPCookies for localhost
// is "localhost.local"
isDomainOK = [cookieDomain isEqual:@"localhost"]
|| [cookieDomain isEqual:@"localhost.local"];
} else {
isDomainOK = [domain hasSuffix:cookieDomain];
}
BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
BOOL isSecureOK = (!cookieIsSecure) || [scheme isEqual:@"https"];
if (isDomainOK && isPathOK && isSecureOK) {
if (foundCookies == nil) {
foundCookies = [NSMutableArray arrayWithCapacity:1];
}
[foundCookies addObject:storedCookie];
}
}
}
return foundCookies;
}
// return a cookie from the array with the same name, domain, and path as the
// given cookie, or else return nil if none found
//
// Both the cookie being tested and all cookies in the storage array should
// be valid (non-nil name, domains, paths)
//
// note: this should only be called from inside a @synchronized(cookies_) block
- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie {
NSUInteger numberOfCookies = [cookies_ count];
NSString *name = [cookie name];
NSString *domain = [cookie domain];
NSString *path = [cookie path];
NSAssert3(name && domain && path, @"Invalid cookie (name:%@ domain:%@ path:%@)",
name, domain, path);
for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
if ([[storedCookie name] isEqual:name]
&& [[storedCookie domain] isEqual:domain]
&& [[storedCookie path] isEqual:path]) {
return storedCookie;
}
}
return nil;
}
// internal routine to remove any expired cookies from the array, excluding
// cookies with nil expirations
//
// note: this should only be called from inside a @synchronized(cookies_) block
- (void)removeExpiredCookies {
// count backwards since we're deleting items from the array
for (NSInteger idx = [cookies_ count] - 1; idx >= 0; idx--) {
NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
NSDate *expiresDate = [storedCookie expiresDate];
if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) {
[cookies_ removeObjectAtIndex:idx];
}
}
}
- (void)removeAllCookies {
@synchronized(cookies_) {
[cookies_ removeAllObjects];
}
}
@end
//
// GTMCachedURLResponse
//
@implementation GTMCachedURLResponse
@synthesize response = response_;
@synthesize data = data_;
@synthesize reservationDate = reservationDate_;
@synthesize useDate = useDate_;
- (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data {
self = [super init];
if (self != nil) {
response_ = [response retain];
data_ = [data retain];
useDate_ = [[NSDate alloc] init];
}
return self;
}
- (void)dealloc {
[response_ release];
[data_ release];
[useDate_ release];
[reservationDate_ release];
[super dealloc];
}
- (NSString *)description {
NSString *reservationStr = reservationDate_ ?
[NSString stringWithFormat:@" resDate:%@", reservationDate_] : @"";
return [NSString stringWithFormat:@"%@ %p: {bytes:%@ useDate:%@%@}",
[self class], self,
data_ ? [NSNumber numberWithInt:(int)[data_ length]] : nil,
useDate_,
reservationStr];
}
- (NSComparisonResult)compareUseDate:(GTMCachedURLResponse *)other {
return [useDate_ compare:[other useDate]];
}
@end
//
// GTMURLCache
//
@implementation GTMURLCache
@dynamic memoryCapacity;
- (id)init {
return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity];
}
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes {
self = [super init];
if (self != nil) {
memoryCapacity_ = totalBytes;
responses_ = [[NSMutableDictionary alloc] initWithCapacity:5];
reservationInterval_ = kCachedURLReservationInterval;
}
return self;
}
- (void)dealloc {
[responses_ release];
[super dealloc];
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@ %p: {responses:%@}",
[self class], self, [responses_ allValues]];
}
// setters/getters
- (void)pruneCacheResponses {
// internal routine to remove the least-recently-used responses when the
// cache has grown too large
if (memoryCapacity_ >= totalDataSize_) return;
// sort keys by date
SEL sel = @selector(compareUseDate:);
NSArray *sortedKeys = [responses_ keysSortedByValueUsingSelector:sel];
// the least-recently-used keys are at the beginning of the sorted array;
// remove those (except ones still reserved) until the total data size is
// reduced sufficiently
for (NSURL *key in sortedKeys) {
GTMCachedURLResponse *response = [responses_ objectForKey:key];
NSDate *resDate = [response reservationDate];
BOOL isResponseReserved = (resDate != nil)
&& ([resDate timeIntervalSinceNow] > -reservationInterval_);
if (!isResponseReserved) {
// we can remove this response from the cache
NSUInteger storedSize = [[response data] length];
totalDataSize_ -= storedSize;
[responses_ removeObjectForKey:key];
}
// if we've removed enough response data, then we're done
if (memoryCapacity_ >= totalDataSize_) break;
}
}
- (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse
forRequest:(NSURLRequest *)request {
@synchronized(self) {
// remove any previous entry for this request
[self removeCachedResponseForRequest:request];
// cache this one only if it's not bigger than our cache
NSUInteger storedSize = [[cachedResponse data] length];
if (storedSize < memoryCapacity_) {
NSURL *key = [request URL];
[responses_ setObject:cachedResponse forKey:key];
totalDataSize_ += storedSize;
[self pruneCacheResponses];
}
}
}
- (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
GTMCachedURLResponse *response;
@synchronized(self) {
NSURL *key = [request URL];
response = [[[responses_ objectForKey:key] retain] autorelease];
// touch the date to indicate this was recently retrieved
[response setUseDate:[NSDate date]];
}
return response;
}
- (void)removeCachedResponseForRequest:(NSURLRequest *)request {
@synchronized(self) {
NSURL *key = [request URL];
totalDataSize_ -= [[[responses_ objectForKey:key] data] length];
[responses_ removeObjectForKey:key];
}
}
- (void)removeAllCachedResponses {
@synchronized(self) {
[responses_ removeAllObjects];
totalDataSize_ = 0;
}
}
- (NSUInteger)memoryCapacity {
return memoryCapacity_;
}
- (void)setMemoryCapacity:(NSUInteger)totalBytes {
@synchronized(self) {
BOOL didShrink = (totalBytes < memoryCapacity_);
memoryCapacity_ = totalBytes;
if (didShrink) {
[self pruneCacheResponses];
}
}
}
// methods for unit testing
- (void)setReservationInterval:(NSTimeInterval)secs {
reservationInterval_ = secs;
}
- (NSDictionary *)responses {
return responses_;
}
- (NSUInteger)totalDataSize {
return totalDataSize_;
}
@end
//
// GTMHTTPFetchHistory
//
@interface GTMHTTPFetchHistory ()
- (NSString *)cachedETagForRequest:(NSURLRequest *)request;
- (void)removeCachedDataForRequest:(NSURLRequest *)request;
@end
@implementation GTMHTTPFetchHistory
@synthesize cookieStorage = cookieStorage_;
@dynamic shouldRememberETags;
@dynamic shouldCacheETaggedData;
@dynamic memoryCapacity;
- (id)init {
return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity
shouldCacheETaggedData:NO];
}
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes
shouldCacheETaggedData:(BOOL)shouldCacheETaggedData {
self = [super init];
if (self != nil) {
etaggedDataCache_ = [[GTMURLCache alloc] initWithMemoryCapacity:totalBytes];
shouldRememberETags_ = shouldCacheETaggedData;
shouldCacheETaggedData_ = shouldCacheETaggedData;
cookieStorage_ = [[GTMCookieStorage alloc] init];
}
return self;
}
- (void)dealloc {
[etaggedDataCache_ release];
[cookieStorage_ release];
[super dealloc];
}
- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet {
if ([self shouldRememberETags]) {
// If this URL is in the history, and no ETag has been set, then
// set the ETag header field
// if we have a history, we're tracking across fetches, so we don't
// want to pull results from any other cache
[request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
if (isHTTPGet) {
// we'll only add an ETag if there's no ETag specified in the user's
// request
NSString *specifiedETag = [request valueForHTTPHeaderField:kGTMIfNoneMatchHeader];
if (specifiedETag == nil) {
// no ETag: extract the previous ETag for this request from the
// fetch history, and add it to the request
NSString *cachedETag = [self cachedETagForRequest:request];
if (cachedETag != nil) {
[request addValue:cachedETag forHTTPHeaderField:kGTMIfNoneMatchHeader];
}
} else {
// has an ETag: remove any stored response in the fetch history
// for this request, as the If-None-Match header could lead to
// a 304 Not Modified, and we want that error delivered to the
// user since they explicitly specified the ETag
[self removeCachedDataForRequest:request];
}
}
}
}
- (void)updateFetchHistoryWithRequest:(NSURLRequest *)request
response:(NSURLResponse *)response
downloadedData:(NSData *)downloadedData {
if (![self shouldRememberETags]) return;
if (![response respondsToSelector:@selector(allHeaderFields)]) return;
NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
if (statusCode != kGTMHTTPFetcherStatusNotModified) {
// save this ETag string for successful results (<300)
// If there's no last modified string, clear the dictionary
// entry for this URL. Also cache or delete the data, if appropriate
// (when etaggedDataCache is non-nil.)
NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
NSString* etag = [headers objectForKey:kGTMETagHeader];
if (etag != nil && statusCode < 300) {
// we want to cache responses for the headers, even if the client
// doesn't want the response body data caches
NSData *dataToStore = shouldCacheETaggedData_ ? downloadedData : nil;
GTMCachedURLResponse *cachedResponse;
cachedResponse = [[[GTMCachedURLResponse alloc] initWithResponse:response
data:dataToStore] autorelease];
[etaggedDataCache_ storeCachedResponse:cachedResponse
forRequest:request];
} else {
[etaggedDataCache_ removeCachedResponseForRequest:request];
}
}
}
- (NSString *)cachedETagForRequest:(NSURLRequest *)request {
GTMCachedURLResponse *cachedResponse;
cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
NSURLResponse *response = [cachedResponse response];
NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
NSString *cachedETag = [headers objectForKey:kGTMETagHeader];
if (cachedETag) {
// since the request having an ETag implies this request is about
// to be fetched again, reserve the cached response to ensure that
// that it will be around at least until the fetch completes
//
// when the fetch completes, either the cached response will be replaced
// with a new response, or the cachedDataForRequest: method below will
// clear the reservation
[cachedResponse setReservationDate:[NSDate date]];
}
return cachedETag;
}
- (NSData *)cachedDataForRequest:(NSURLRequest *)request {
GTMCachedURLResponse *cachedResponse;
cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
NSData *cachedData = [cachedResponse data];
// since the data for this cached request is being obtained from the cache,
// we can clear the reservation as the fetch has completed
[cachedResponse setReservationDate:nil];
return cachedData;
}
- (void)removeCachedDataForRequest:(NSURLRequest *)request {
[etaggedDataCache_ removeCachedResponseForRequest:request];
}
- (void)clearETaggedDataCache {
[etaggedDataCache_ removeAllCachedResponses];
}
- (void)clearHistory {
[self clearETaggedDataCache];
[cookieStorage_ removeAllCookies];
}
- (void)removeAllCookies {
[cookieStorage_ removeAllCookies];
}
- (BOOL)shouldRememberETags {
return shouldRememberETags_;
}
- (void)setShouldRememberETags:(BOOL)flag {
BOOL wasRemembering = shouldRememberETags_;
shouldRememberETags_ = flag;
if (wasRemembering && !flag) {
// free up the cache memory
[self clearETaggedDataCache];
}
}
- (BOOL)shouldCacheETaggedData {
return shouldCacheETaggedData_;
}
- (void)setShouldCacheETaggedData:(BOOL)flag {
BOOL wasCaching = shouldCacheETaggedData_;
shouldCacheETaggedData_ = flag;
if (flag) {
self.shouldRememberETags = YES;
}
if (wasCaching && !flag) {
// users expect turning off caching to free up the cache memory
[self clearETaggedDataCache];
}
}
- (NSUInteger)memoryCapacity {
return [etaggedDataCache_ memoryCapacity];
}
- (void)setMemoryCapacity:(NSUInteger)totalBytes {
[etaggedDataCache_ setMemoryCapacity:totalBytes];
}
@end

704
OAuth 2/GTMHTTPFetcher.h Executable file
View File

@@ -0,0 +1,704 @@
/* 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.
*/
//
// GTMHTTPFetcher.h
//
// This is essentially a wrapper around NSURLConnection for POSTs and GETs.
// If setPostData: is called, then POST is assumed.
//
// When would you use this instead of NSURLConnection?
//
// - When you just want the result from a GET, POST, or PUT
// - When you want the "standard" behavior for connections (redirection handling
// an so on)
// - When you want automatic retry on failures
// - When you want to avoid cookie collisions with Safari and other applications
// - When you are fetching resources with ETags and want to avoid the overhead
// of repeated fetches of unchanged data
// - When you need to set a credential for the http operation
//
// This is assumed to be a one-shot fetch request; don't reuse the object
// for a second fetch.
//
// The fetcher may be created auto-released, in which case it will release
// itself after the fetch completion callback. The fetcher is implicitly
// retained as long as a connection is pending.
//
// But if you may need to cancel the fetcher, retain it and have the delegate
// release the fetcher in the callbacks.
//
// Sample usage:
//
// NSURLRequest *request = [NSURLRequest requestWithURL:myURL];
// GTMHTTPFetcher* myFetcher = [GTMHTTPFetcher fetcherWithRequest:request];
//
// // optional upload body data
// [myFetcher setPostData:[postString dataUsingEncoding:NSUTF8StringEncoding]];
//
// [myFetcher beginFetchWithDelegate:self
// didFinishSelector:@selector(myFetcher:finishedWithData:error:)];
//
// Upon fetch completion, the callback selector is invoked; it should have
// this signature (you can use any callback method name you want so long as
// the signature matches this):
//
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)retrievedData error:(NSError *)error;
//
// The block callback version looks like:
//
// [myFetcher beginFetchWithCompletionHandler:^(NSData *retrievedData, NSError *error) {
// if (error != nil) {
// // status code or network error
// } else {
// // succeeded
// }
// }];
//
// NOTE: Fetches may retrieve data from the server even though the server
// returned an error. The failure selector is called when the server
// status is >= 300, with an NSError having domain
// kGTMHTTPFetcherStatusDomain and code set to the server status.
//
// Status codes are at <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html>
//
//
// Downloading to disk:
//
// To have downloaded data saved directly to disk, specify either a path for the
// downloadPath property, or a file handle for the downloadFileHandle property.
// When downloading to disk, callbacks will be passed a nil for the NSData*
// arguments.
//
//
// HTTP methods and headers:
//
// Alternative HTTP methods, like PUT, and custom headers can be specified by
// creating the fetcher with an appropriate NSMutableURLRequest
//
//
// Proxies:
//
// Proxy handling is invisible so long as the system has a valid credential in
// the keychain, which is normally true (else most NSURL-based apps would have
// difficulty.) But when there is a proxy authetication error, the the fetcher
// will call the failedWithError: method with the NSURLChallenge in the error's
// userInfo. The error method can get the challenge info like this:
//
// NSURLAuthenticationChallenge *challenge
// = [[error userInfo] objectForKey:kGTMHTTPFetcherErrorChallengeKey];
// BOOL isProxyChallenge = [[challenge protectionSpace] isProxy];
//
// If a proxy error occurs, you can ask the user for the proxy username/password
// and call fetcher's setProxyCredential: to provide those for the
// next attempt to fetch.
//
//
// Cookies:
//
// There are three supported mechanisms for remembering cookies between fetches.
//
// By default, GTMHTTPFetcher uses a mutable array held statically to track
// cookies for all instantiated fetchers. This avoids server cookies being set
// by servers for the application from interfering with Safari cookie settings,
// and vice versa. The fetcher cookies are lost when the application quits.
//
// To rely instead on WebKit's global NSHTTPCookieStorage, call
// setCookieStorageMethod: with kGTMHTTPFetcherCookieStorageMethodSystemDefault.
//
// If the fetcher is created from a GTMHTTPFetcherService object
// then the cookie storage mechanism is set to use the cookie storage in the
// service object rather than the static storage.
//
//
// Fetching for periodic checks:
//
// The fetcher object tracks ETag headers from responses and
// provide an "If-None-Match" header. This allows the server to save
// bandwidth by providing a status message instead of repeated response
// data.
//
// To get this behavior, create the fetcher from an GTMHTTPFetcherService object
// and look for a fetch callback error with code 304
// (kGTMHTTPFetcherStatusNotModified) like this:
//
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error {
// if ([error code] == kGTMHTTPFetcherStatusNotModified) {
// // |data| is empty; use the data from the previous finishedWithData: for this URL
// } else {
// // handle other server status code
// }
// }
//
//
// Monitoring received data
//
// The optional received data selector can be set with setReceivedDataSelector:
// and should have the signature
//
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher receivedData:(NSData *)dataReceivedSoFar;
//
// The number bytes received so far is available as [fetcher downloadedLength].
// This number may go down if a redirect causes the download to begin again from
// a new server.
//
// If supplied by the server, the anticipated total download size is available
// as [[myFetcher response] expectedContentLength] (and may be -1 for unknown
// download sizes.)
//
//
// Automatic retrying of fetches
//
// The fetcher can optionally create a timer and reattempt certain kinds of
// fetch failures (status codes 408, request timeout; 503, service unavailable;
// 504, gateway timeout; networking errors NSURLErrorTimedOut and
// NSURLErrorNetworkConnectionLost.) The user may set a retry selector to
// customize the type of errors which will be retried.
//
// Retries are done in an exponential-backoff fashion (that is, after 1 second,
// 2, 4, 8, and so on.)
//
// Enabling automatic retries looks like this:
// [myFetcher setRetryEnabled:YES];
//
// With retries enabled, the success or failure callbacks are called only
// when no more retries will be attempted. Calling the fetcher's stopFetching
// method will terminate the retry timer, without the finished or failure
// selectors being invoked.
//
// Optionally, the client may set the maximum retry interval:
// [myFetcher setMaxRetryInterval:60.0]; // in seconds; default is 60 seconds
// // for downloads, 600 for uploads
//
// Also optionally, the client may provide a callback selector to determine
// if a status code or other error should be retried.
// [myFetcher setRetrySelector:@selector(myFetcher:willRetry:forError:)];
//
// If set, the retry selector should have the signature:
// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error
// and return YES to set the retry timer or NO to fail without additional
// fetch attempts.
//
// The retry method may return the |suggestedWillRetry| argument to get the
// default retry behavior. Server status codes are present in the
// error argument, and have the domain kGTMHTTPFetcherStatusDomain. The
// user's method may look something like this:
//
// -(BOOL)myFetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error {
//
// // perhaps examine [error domain] and [error code], or [fetcher retryCount]
// //
// // return YES to start the retry timer, NO to proceed to the failure
// // callback, or |suggestedWillRetry| to get default behavior for the
// // current error domain and code values.
// return suggestedWillRetry;
// }
#pragma once
#import <Foundation/Foundation.h>
#if defined(GTL_TARGET_NAMESPACE)
// we're using target namespace macros
#import "GTLDefines.h"
#elif defined(GDATA_TARGET_NAMESPACE)
#import "GDataDefines.h"
#else
#if TARGET_OS_IPHONE
#ifndef GTM_FOUNDATION_ONLY
#define GTM_FOUNDATION_ONLY 1
#endif
#ifndef GTM_IPHONE
#define GTM_IPHONE 1
#endif
#endif
#endif
#if TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 40000)
#define GTM_BACKGROUND_FETCHING 1
#endif
#undef _EXTERN
#undef _INITIALIZE_AS
#ifdef GTMHTTPFETCHER_DEFINE_GLOBALS
#define _EXTERN
#define _INITIALIZE_AS(x) =x
#else
#if defined(__cplusplus)
#define _EXTERN extern "C"
#else
#define _EXTERN extern
#endif
#define _INITIALIZE_AS(x)
#endif
// notifications
//
// fetch started and stopped, and fetch retry delay started and stopped
_EXTERN NSString* const kGTMHTTPFetcherStartedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherStartedNotification");
_EXTERN NSString* const kGTMHTTPFetcherStoppedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherStoppedNotification");
_EXTERN NSString* const kGTMHTTPFetcherRetryDelayStartedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherRetryDelayStartedNotification");
_EXTERN NSString* const kGTMHTTPFetcherRetryDelayStoppedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherRetryDelayStoppedNotification");
// callback constants
_EXTERN NSString* const kGTMHTTPFetcherErrorDomain _INITIALIZE_AS(@"com.google.GTMHTTPFetcher");
_EXTERN NSString* const kGTMHTTPFetcherStatusDomain _INITIALIZE_AS(@"com.google.HTTPStatus");
_EXTERN NSString* const kGTMHTTPFetcherErrorChallengeKey _INITIALIZE_AS(@"challenge");
_EXTERN NSString* const kGTMHTTPFetcherStatusDataKey _INITIALIZE_AS(@"data"); // data returned with a kGTMHTTPFetcherStatusDomain error
enum {
kGTMHTTPFetcherErrorDownloadFailed = -1,
kGTMHTTPFetcherErrorAuthenticationChallengeFailed = -2,
kGTMHTTPFetcherErrorChunkUploadFailed = -3,
kGTMHTTPFetcherErrorFileHandleException = -4,
kGTMHTTPFetcherErrorBackgroundExpiration = -6,
// The code kGTMHTTPFetcherErrorAuthorizationFailed (-5) has been removed;
// look for status 401 instead.
kGTMHTTPFetcherStatusNotModified = 304,
kGTMHTTPFetcherStatusBadRequest = 400,
kGTMHTTPFetcherStatusUnauthorized = 401,
kGTMHTTPFetcherStatusForbidden = 403,
kGTMHTTPFetcherStatusPreconditionFailed = 412
};
// cookie storage methods
enum {
kGTMHTTPFetcherCookieStorageMethodStatic = 0,
kGTMHTTPFetcherCookieStorageMethodFetchHistory = 1,
kGTMHTTPFetcherCookieStorageMethodSystemDefault = 2,
kGTMHTTPFetcherCookieStorageMethodNone = 3
};
#ifdef __cplusplus
extern "C" {
#endif
void GTMAssertSelectorNilOrImplementedWithArgs(id obj, SEL sel, ...);
// Utility functions for applications self-identifying to servers via a
// user-agent header
// Make a proper app name without whitespace from the given string, removing
// whitespace and other characters that may be special parsed marks of
// the full user-agent string.
NSString *GTMCleanedUserAgentString(NSString *str);
// Make an identifier like "MacOSX/10.7.1" or "iPod_Touch/4.1"
NSString *GTMSystemVersionString(void);
// Make a generic name and version for the current application, like
// com.example.MyApp/1.2.3 relying on the bundle identifier and the
// CFBundleShortVersionString or CFBundleVersion. If no bundle ID
// is available, the process name preceded by "proc_" is used.
NSString *GTMApplicationIdentifier(NSBundle *bundle);
#ifdef __cplusplus
} // extern "C"
#endif
@class GTMHTTPFetcher;
@protocol GTMCookieStorageProtocol <NSObject>
// This protocol allows us to call into the service without requiring
// GTMCookieStorage sources in this project
//
// The public interface for cookie handling is the GTMCookieStorage class,
// accessible from a fetcher service object's fetchHistory or from the fetcher's
// +staticCookieStorage method.
- (NSArray *)cookiesForURL:(NSURL *)theURL;
- (void)setCookies:(NSArray *)newCookies;
@end
@protocol GTMHTTPFetchHistoryProtocol <NSObject>
// This protocol allows us to call the fetch history object without requiring
// GTMHTTPFetchHistory sources in this project
- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet;
- (BOOL)shouldCacheETaggedData;
- (NSData *)cachedDataForRequest:(NSURLRequest *)request;
- (id <GTMCookieStorageProtocol>)cookieStorage;
- (void)updateFetchHistoryWithRequest:(NSURLRequest *)request
response:(NSURLResponse *)response
downloadedData:(NSData *)downloadedData;
- (void)removeCachedDataForRequest:(NSURLRequest *)request;
@end
@protocol GTMHTTPFetcherServiceProtocol <NSObject>
// This protocol allows us to call into the service without requiring
// GTMHTTPFetcherService sources in this project
- (BOOL)fetcherShouldBeginFetching:(GTMHTTPFetcher *)fetcher;
- (void)fetcherDidStop:(GTMHTTPFetcher *)fetcher;
- (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request;
- (BOOL)isDelayingFetcher:(GTMHTTPFetcher *)fetcher;
@end
@protocol GTMFetcherAuthorizationProtocol <NSObject>
@required
// This protocol allows us to call the authorizer without requiring its sources
// in this project
- (void)authorizeRequest:(NSMutableURLRequest *)request
delegate:(id)delegate
didFinishSelector:(SEL)sel;
- (void)stopAuthorization;
- (BOOL)isAuthorizingRequest:(NSURLRequest *)request;
- (BOOL)isAuthorizedRequest:(NSURLRequest *)request;
- (NSString *)userEmail;
@optional
@property (assign) id <GTMHTTPFetcherServiceProtocol> fetcherService; // WEAK
- (BOOL)primeForRefresh;
@end
// GTMHTTPFetcher objects are used for async retrieval of an http get or post
//
// See additional comments at the beginning of this file
@interface GTMHTTPFetcher : NSObject {
@protected
NSMutableURLRequest *request_;
NSURLConnection *connection_;
NSMutableData *downloadedData_;
NSString *downloadPath_;
NSString *temporaryDownloadPath_;
NSFileHandle *downloadFileHandle_;
unsigned long long downloadedLength_;
NSURLCredential *credential_; // username & password
NSURLCredential *proxyCredential_; // credential supplied to proxy servers
NSData *postData_;
NSInputStream *postStream_;
NSMutableData *loggedStreamData_;
NSURLResponse *response_; // set in connection:didReceiveResponse:
id delegate_;
SEL finishedSel_; // should by implemented by delegate
SEL sentDataSel_; // optional, set with setSentDataSelector
SEL receivedDataSel_; // optional, set with setReceivedDataSelector
#if NS_BLOCKS_AVAILABLE
void (^completionBlock_)(NSData *, NSError *);
void (^receivedDataBlock_)(NSData *);
void (^sentDataBlock_)(NSInteger, NSInteger, NSInteger);
BOOL (^retryBlock_)(BOOL, NSError *);
#elif !__LP64__
// placeholders: for 32-bit builds, keep the size of the object's ivar section
// the same with and without blocks
id completionPlaceholder_;
id receivedDataPlaceholder_;
id sentDataPlaceholder_;
id retryPlaceholder_;
#endif
BOOL hasConnectionEnded_; // set if the connection need not be cancelled
BOOL isCancellingChallenge_; // set only when cancelling an auth challenge
BOOL isStopNotificationNeeded_; // set when start notification has been sent
BOOL shouldFetchInBackground_;
#if GTM_BACKGROUND_FETCHING
NSUInteger backgroundTaskIdentifer_; // UIBackgroundTaskIdentifier
#endif
id userData_; // retained, if set by caller
NSMutableDictionary *properties_; // more data retained for caller
NSArray *runLoopModes_; // optional, for 10.5 and later
id <GTMHTTPFetchHistoryProtocol> fetchHistory_; // if supplied by the caller, used for Last-Modified-Since checks and cookies
NSInteger cookieStorageMethod_; // constant from above
id <GTMCookieStorageProtocol> cookieStorage_;
id <GTMFetcherAuthorizationProtocol> authorizer_;
// the service object that created and monitors this fetcher, if any
id <GTMHTTPFetcherServiceProtocol> service_;
NSString *serviceHost_;
NSInteger servicePriority_;
NSThread *thread_;
BOOL isRetryEnabled_; // user wants auto-retry
SEL retrySel_; // optional; set with setRetrySelector
NSTimer *retryTimer_;
NSUInteger retryCount_;
NSTimeInterval maxRetryInterval_; // default 600 seconds
NSTimeInterval minRetryInterval_; // random between 1 and 2 seconds
NSTimeInterval retryFactor_; // default interval multiplier is 2
NSTimeInterval lastRetryInterval_;
BOOL hasAttemptedAuthRefresh_;
NSString *comment_; // comment for log
NSString *log_;
}
// Create a fetcher
//
// fetcherWithRequest will return an autoreleased fetcher, but if
// the connection is successfully created, the connection should retain the
// fetcher for the life of the connection as well. So the caller doesn't have
// to retain the fetcher explicitly unless they want to be able to cancel it.
+ (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request;
// Convenience methods that make a request, like +fetcherWithRequest
+ (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL;
+ (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString;
// Designated initializer
- (id)initWithRequest:(NSURLRequest *)request;
// Fetcher request
//
// The underlying request is mutable and may be modified by the caller
@property (retain) NSMutableURLRequest *mutableRequest;
// Setting the credential is optional; it is used if the connection receives
// an authentication challenge
@property (retain) NSURLCredential *credential;
// Setting the proxy credential is optional; it is used if the connection
// receives an authentication challenge from a proxy
@property (retain) NSURLCredential *proxyCredential;
// If post data or stream is not set, then a GET retrieval method is assumed
@property (retain) NSData *postData;
@property (retain) NSInputStream *postStream;
// The default cookie storage method is kGTMHTTPFetcherCookieStorageMethodStatic
// without a fetch history set, and kGTMHTTPFetcherCookieStorageMethodFetchHistory
// with a fetch history set
//
// Applications needing control of cookies across a sequence of fetches should
// create fetchers from a GTMHTTPFetcherService object (which encapsulates
// fetch history) for a well-defined cookie store
@property (assign) NSInteger cookieStorageMethod;
+ (id <GTMCookieStorageProtocol>)staticCookieStorage;
// Object to add authorization to the request, if needed
@property (retain) id <GTMFetcherAuthorizationProtocol> authorizer;
// The service object that created and monitors this fetcher, if any
@property (retain) id <GTMHTTPFetcherServiceProtocol> service;
// The host, if any, used to classify this fetcher in the fetcher service
@property (copy) NSString *serviceHost;
// The priority, if any, used for starting fetchers in the fetcher service
//
// Lower values are higher priority; the default is 0, and values may
// be negative or positive. This priority affects only the start order of
// fetchers that are being delayed by a fetcher service.
@property (assign) NSInteger servicePriority;
// The thread used to run this fetcher in the fetcher service
@property (retain) NSThread *thread;
// The delegate is retained during the connection
@property (retain) id delegate;
// On iOS 4 and later, the fetch may optionally continue while the app is in the
// background until finished or stopped by OS expiration
//
// The default value is NO
//
// For Mac OS X, background fetches are always supported, and this property
// is ignored
@property (assign) BOOL shouldFetchInBackground;
// The delegate's optional sentData selector may be used to monitor upload
// progress. It should have a signature like:
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher
// didSendBytes:(NSInteger)bytesSent
// totalBytesSent:(NSInteger)totalBytesSent
// totalBytesExpectedToSend:(NSInteger)totalBytesExpectedToSend;
//
// +doesSupportSentDataCallback indicates if this delegate method is supported
+ (BOOL)doesSupportSentDataCallback;
@property (assign) SEL sentDataSelector;
// The delegate's optional receivedData selector may be used to monitor download
// progress. It should have a signature like:
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher
// receivedData:(NSData *)dataReceivedSoFar;
//
// The dataReceived argument will be nil when downloading to a path or to a
// file handle.
//
// Applications should not use this method to accumulate the received data;
// the callback method or block supplied to the beginFetch call will have
// the complete NSData received.
@property (assign) SEL receivedDataSelector;
#if NS_BLOCKS_AVAILABLE
// The full interface to the block is provided rather than just a typedef for
// its parameter list in order to get more useful code completion in the Xcode
// editor
@property (copy) void (^sentDataBlock)(NSInteger bytesSent, NSInteger totalBytesSent, NSInteger bytesExpectedToSend);
// The dataReceived argument will be nil when downloading to a path or to
// a file handle
@property (copy) void (^receivedDataBlock)(NSData *dataReceivedSoFar);
#endif
// retrying; see comments at the top of the file. Calling
// setRetryEnabled(YES) resets the min and max retry intervals.
@property (assign, getter=isRetryEnabled) BOOL retryEnabled;
// Retry selector or block is optional for retries.
//
// If present, it should have the signature:
// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error
// and return YES to cause a retry. See comments at the top of this file.
@property (assign) SEL retrySelector;
#if NS_BLOCKS_AVAILABLE
@property (copy) BOOL (^retryBlock)(BOOL suggestedWillRetry, NSError *error);
#endif
// Retry intervals must be strictly less than maxRetryInterval, else
// they will be limited to maxRetryInterval and no further retries will
// be attempted. Setting maxRetryInterval to 0.0 will reset it to the
// default value, 600 seconds.
@property (assign) NSTimeInterval maxRetryInterval;
// Starting retry interval. Setting minRetryInterval to 0.0 will reset it
// to a random value between 1.0 and 2.0 seconds. Clients should normally not
// call this except for unit testing.
@property (assign) NSTimeInterval minRetryInterval;
// Multiplier used to increase the interval between retries, typically 2.0.
// Clients should not need to call this.
@property (assign) double retryFactor;
// Number of retries attempted
@property (readonly) NSUInteger retryCount;
// interval delay to precede next retry
@property (readonly) NSTimeInterval nextRetryInterval;
// Begin fetching the request
//
// The delegate can optionally implement the finished selectors or pass NULL
// for it.
//
// Returns YES if the fetch is initiated. The delegate is retained between
// the beginFetch call until after the finish callback.
//
// An error is passed to the callback for server statuses 300 or
// higher, with the status stored as the error object's code.
//
// finishedSEL has a signature like:
// - (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error;
//
// If the application has specified a downloadPath or downloadFileHandle
// for the fetcher, the data parameter passed to the callback will be nil.
- (BOOL)beginFetchWithDelegate:(id)delegate
didFinishSelector:(SEL)finishedSEL;
#if NS_BLOCKS_AVAILABLE
- (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler;
#endif
// Returns YES if this is in the process of fetching a URL
- (BOOL)isFetching;
// Cancel the fetch of the request that's currently in progress
- (void)stopFetching;
// Return the status code from the server response
@property (readonly) NSInteger statusCode;
// Return the http headers from the response
@property (retain, readonly) NSDictionary *responseHeaders;
// The response, once it's been received
@property (retain) NSURLResponse *response;
// Bytes downloaded so far
@property (readonly) unsigned long long downloadedLength;
// Buffer of currently-downloaded data
@property (readonly, retain) NSData *downloadedData;
// Path in which to non-atomically create a file for storing the downloaded data
//
// The path must be set before fetching begins. The download file handle
// will be created for the path, and can be used to monitor progress. If a file
// already exists at the path, it will be overwritten.
@property (copy) NSString *downloadPath;
// If downloadFileHandle is set, data received is immediately appended to
// the file handle rather than being accumulated in the downloadedData property
//
// The file handle supplied must allow writing and support seekToFileOffset:,
// and must be set before fetching begins. Setting a download path will
// override the file handle property.
@property (retain) NSFileHandle *downloadFileHandle;
// The optional fetchHistory object is used for a sequence of fetchers to
// remember ETags, cache ETagged data, and store cookies. Typically, this
// is set by a GTMFetcherService object when it creates a fetcher.
//
// Side effect: setting fetch history implicitly calls setCookieStorageMethod:
@property (retain) id <GTMHTTPFetchHistoryProtocol> fetchHistory;
// userData is retained for the convenience of the caller
@property (retain) id userData;
// Stored property values are retained for the convenience of the caller
@property (copy) NSMutableDictionary *properties;
- (void)setProperty:(id)obj forKey:(NSString *)key; // pass nil obj to remove property
- (id)propertyForKey:(NSString *)key;
- (void)addPropertiesFromDictionary:(NSDictionary *)dict;
// Comments are useful for logging
@property (copy) NSString *comment;
- (void)setCommentWithFormat:(id)format, ...;
// Log of request and response, if logging is enabled
@property (copy) NSString *log;
// Using the fetcher while a modal dialog is displayed requires setting the
// run-loop modes to include NSModalPanelRunLoopMode
@property (retain) NSArray *runLoopModes;
// Users who wish to replace GTMHTTPFetcher's use of NSURLConnection
// can do so globally here. The replacement should be a subclass of
// NSURLConnection.
+ (Class)connectionClass;
+ (void)setConnectionClass:(Class)theClass;
// Spin the run loop, discarding events, until the fetch has completed
//
// This is only for use in testing or in tools without a user interface.
//
// Synchronous fetches should never be done by shipping apps; they are
// sufficient reason for rejection from the app store.
- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds;
#if STRIP_GTM_FETCH_LOGGING
// if logging is stripped, provide a stub for the main method
// for controlling logging
+ (void)setLoggingEnabled:(BOOL)flag;
#endif // STRIP_GTM_FETCH_LOGGING
@end

1746
OAuth 2/GTMHTTPFetcher.m Executable file

File diff suppressed because it is too large Load Diff

325
OAuth 2/GTMOAuth2Authentication.h Executable file
View File

@@ -0,0 +1,325 @@
/* 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.
*/
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
// This class implements the OAuth 2 protocol for authorizing requests.
// http://tools.ietf.org/html/draft-ietf-oauth-v2
#import <Foundation/Foundation.h>
// GTMHTTPFetcher.h brings in GTLDefines/GDataDefines
#import "GTMHTTPFetcher.h"
#undef _EXTERN
#undef _INITIALIZE_AS
#ifdef GTMOAUTH2AUTHENTICATION_DEFINE_GLOBALS
#define _EXTERN
#define _INITIALIZE_AS(x) =x
#else
#if defined(__cplusplus)
#define _EXTERN extern "C"
#else
#define _EXTERN extern
#endif
#define _INITIALIZE_AS(x)
#endif
// Until all OAuth 2 providers are up to the same spec, we'll provide a crude
// way here to override the "Bearer" string in the Authorization header
#ifndef GTM_OAUTH2_BEARER
#define GTM_OAUTH2_BEARER "Bearer"
#endif
// Service provider name allows stored authorization to be associated with
// the authorizing service
_EXTERN NSString* const kGTMOAuth2ServiceProviderGoogle _INITIALIZE_AS(@"Google");
//
// GTMOAuth2SignIn constants, included here for use by clients
//
_EXTERN NSString* const kGTMOAuth2ErrorDomain _INITIALIZE_AS(@"com.google.GTMOAuth2");
// Error userInfo keys
_EXTERN NSString* const kGTMOAuth2ErrorMessageKey _INITIALIZE_AS(@"error");
_EXTERN NSString* const kGTMOAuth2ErrorRequestKey _INITIALIZE_AS(@"request");
_EXTERN NSString* const kGTMOAuth2ErrorJSONKey _INITIALIZE_AS(@"json");
enum {
// Error code indicating that the window was prematurely closed
kGTMOAuth2ErrorWindowClosed = -1000,
kGTMOAuth2ErrorAuthorizationFailed = -1001,
kGTMOAuth2ErrorTokenExpired = -1002,
kGTMOAuth2ErrorTokenUnavailable = -1003,
kGTMOAuth2ErrorUnauthorizableRequest = -1004
};
// Notifications for token fetches
_EXTERN NSString* const kGTMOAuth2FetchStarted _INITIALIZE_AS(@"kGTMOAuth2FetchStarted");
_EXTERN NSString* const kGTMOAuth2FetchStopped _INITIALIZE_AS(@"kGTMOAuth2FetchStopped");
_EXTERN NSString* const kGTMOAuth2FetcherKey _INITIALIZE_AS(@"fetcher");
_EXTERN NSString* const kGTMOAuth2FetchTypeKey _INITIALIZE_AS(@"FetchType");
_EXTERN NSString* const kGTMOAuth2FetchTypeToken _INITIALIZE_AS(@"token");
_EXTERN NSString* const kGTMOAuth2FetchTypeRefresh _INITIALIZE_AS(@"refresh");
_EXTERN NSString* const kGTMOAuth2FetchTypeAssertion _INITIALIZE_AS(@"assertion");
_EXTERN NSString* const kGTMOAuth2FetchTypeUserInfo _INITIALIZE_AS(@"userInfo");
// Token-issuance errors
_EXTERN NSString* const kGTMOAuth2ErrorKey _INITIALIZE_AS(@"error");
_EXTERN NSString* const kGTMOAuth2ErrorInvalidRequest _INITIALIZE_AS(@"invalid_request");
_EXTERN NSString* const kGTMOAuth2ErrorInvalidClient _INITIALIZE_AS(@"invalid_client");
_EXTERN NSString* const kGTMOAuth2ErrorInvalidGrant _INITIALIZE_AS(@"invalid_grant");
_EXTERN NSString* const kGTMOAuth2ErrorUnauthorizedClient _INITIALIZE_AS(@"unauthorized_client");
_EXTERN NSString* const kGTMOAuth2ErrorUnsupportedGrantType _INITIALIZE_AS(@"unsupported_grant_type");
_EXTERN NSString* const kGTMOAuth2ErrorInvalidScope _INITIALIZE_AS(@"invalid_scope");
// Notification that sign-in has completed, and token fetches will begin (useful
// for displaying interstitial messages after the window has closed)
_EXTERN NSString* const kGTMOAuth2UserSignedIn _INITIALIZE_AS(@"kGTMOAuth2UserSignedIn");
// Notification for token changes
_EXTERN NSString* const kGTMOAuth2AccessTokenRefreshed _INITIALIZE_AS(@"kGTMOAuth2AccessTokenRefreshed");
_EXTERN NSString* const kGTMOAuth2RefreshTokenChanged _INITIALIZE_AS(@"kGTMOAuth2RefreshTokenChanged");
// Notification for WebView loading
_EXTERN NSString* const kGTMOAuth2WebViewStartedLoading _INITIALIZE_AS(@"kGTMOAuth2WebViewStartedLoading");
_EXTERN NSString* const kGTMOAuth2WebViewStoppedLoading _INITIALIZE_AS(@"kGTMOAuth2WebViewStoppedLoading");
_EXTERN NSString* const kGTMOAuth2WebViewKey _INITIALIZE_AS(@"kGTMOAuth2WebViewKey");
_EXTERN NSString* const kGTMOAuth2WebViewStopKindKey _INITIALIZE_AS(@"kGTMOAuth2WebViewStopKindKey");
_EXTERN NSString* const kGTMOAuth2WebViewFinished _INITIALIZE_AS(@"finished");
_EXTERN NSString* const kGTMOAuth2WebViewFailed _INITIALIZE_AS(@"failed");
_EXTERN NSString* const kGTMOAuth2WebViewCancelled _INITIALIZE_AS(@"cancelled");
// Notification for network loss during html sign-in display
_EXTERN NSString* const kGTMOAuth2NetworkLost _INITIALIZE_AS(@"kGTMOAuthNetworkLost");
_EXTERN NSString* const kGTMOAuth2NetworkFound _INITIALIZE_AS(@"kGTMOAuthNetworkFound");
@interface GTMOAuth2Authentication : NSObject <GTMFetcherAuthorizationProtocol> {
@private
NSString *clientID_;
NSString *clientSecret_;
NSString *redirectURI_;
NSMutableDictionary *parameters_;
// authorization parameters
NSURL *tokenURL_;
NSDate *expirationDate_;
NSDictionary *additionalTokenRequestParameters_;
// queue of requests for authorization waiting for a valid access token
GTMHTTPFetcher *refreshFetcher_;
NSMutableArray *authorizationQueue_;
id <GTMHTTPFetcherServiceProtocol> fetcherService_; // WEAK
Class parserClass_;
BOOL shouldAuthorizeAllRequests_;
// arbitrary data retained for the user
id userData_;
NSMutableDictionary *properties_;
}
// OAuth2 standard protocol parameters
//
// These should be the plain strings; any needed escaping will be provided by
// the library.
// Request properties
@property (copy) NSString *clientID;
@property (copy) NSString *clientSecret;
@property (copy) NSString *redirectURI;
@property (retain) NSString *scope;
@property (retain) NSString *tokenType;
@property (retain) NSString *assertion;
// Apps may optionally add parameters here to be provided to the token
// endpoint on token requests and refreshes
@property (retain) NSDictionary *additionalTokenRequestParameters;
// Response properties
@property (retain) NSMutableDictionary *parameters;
@property (retain) NSString *accessToken;
@property (retain) NSString *refreshToken;
@property (retain) NSNumber *expiresIn;
@property (retain) NSString *code;
@property (retain) NSString *errorString;
// URL for obtaining access tokens
@property (copy) NSURL *tokenURL;
// Calculated expiration date (expiresIn seconds added to the
// time the access token was received.)
@property (copy) NSDate *expirationDate;
// Service identifier, like "Google"; not used for authentication
//
// The provider name is just for allowing stored authorization to be associated
// with the authorizing service.
@property (copy) NSString *serviceProvider;
// User email and verified status; not used for authentication
//
// The verified string can be checked with -boolValue. If the result is false,
// then the email address is listed with the account on the server, but the
// address has not been confirmed as belonging to the owner of the account.
@property (retain) NSString *userEmail;
@property (retain) NSString *userEmailIsVerified;
// Property indicating if this auth has a refresh token so is suitable for
// authorizing a request. This does not guarantee that the token is valid.
@property (readonly) BOOL canAuthorize;
// Property indicating if this object will authorize plain http request
// (as well as any non-https requests.) Default is NO, only requests with the
// scheme https are authorized, since security may be compromised if tokens
// are sent over the wire using an unencrypted protocol like http.
@property (assign) BOOL shouldAuthorizeAllRequests;
// userData is retained for the convenience of the caller
@property (retain) id userData;
// Stored property values are retained for the convenience of the caller
@property (retain) NSDictionary *properties;
// Property for the optional fetcher service instance to be used to create
// fetchers
//
// Fetcher service objects retain authorizations, so this is weak to avoid
// circular retains.
@property (assign) id <GTMHTTPFetcherServiceProtocol> fetcherService; // WEAK
// Alternative JSON parsing class; this should implement the
// GTMOAuth2ParserClass informal protocol. If this property is
// not set, the class SBJSON must be available in the runtime.
@property (assign) Class parserClass;
// Convenience method for creating an authentication object
+ (id)authenticationWithServiceProvider:(NSString *)serviceProvider
tokenURL:(NSURL *)tokenURL
redirectURI:(NSString *)redirectURI
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret;
// Clear out any authentication values, prepare for a new request fetch
- (void)reset;
// Main authorization entry points
//
// These will refresh the access token, if necessary, add the access token to
// the request, then invoke the callback.
//
// The request argument may be nil to just force a refresh of the access token,
// if needed.
//
// NOTE: To avoid accidental leaks of bearer tokens, the request must
// be for a URL with the scheme https unless the shouldAuthorizeAllRequests
// property is set.
// The finish selector should have a signature matching
// - (void)authentication:(GTMOAuth2Authentication *)auth
// request:(NSMutableURLRequest *)request
// finishedWithError:(NSError *)error;
- (void)authorizeRequest:(NSMutableURLRequest *)request
delegate:(id)delegate
didFinishSelector:(SEL)sel;
#if NS_BLOCKS_AVAILABLE
- (void)authorizeRequest:(NSMutableURLRequest *)request
completionHandler:(void (^)(NSError *error))handler;
#endif
// Synchronous entry point; authorizing this way cannot refresh an expired
// access token
- (BOOL)authorizeRequest:(NSMutableURLRequest *)request;
// If the authentication is waiting for a refresh to complete, spin the run
// loop, discarding events, until the fetch has completed
//
// This is only for use in testing or in tools without a user interface.
- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds;
//////////////////////////////////////////////////////////////////////////////
//
// Internal properties and methods for use by GTMOAuth2SignIn
//
// Pending fetcher to get a new access token, if any
@property (retain) GTMHTTPFetcher *refreshFetcher;
// Check if a request is queued up to be authorized
- (BOOL)isAuthorizingRequest:(NSURLRequest *)request;
// Check if a request appears to be authorized
- (BOOL)isAuthorizedRequest:(NSURLRequest *)request;
// Stop any pending refresh fetch
- (void)stopAuthorization;
// OAuth fetch user-agent header value
- (NSString *)userAgent;
// Parse and set token and token secret from response data
- (void)setKeysForResponseString:(NSString *)str;
- (void)setKeysForResponseDictionary:(NSDictionary *)dict;
// Persistent token string for keychain storage
//
// We'll use the format "refresh_token=foo&serviceProvider=bar" so we can
// easily alter what portions of the auth data are stored
//
// Use these methods for serialization
- (NSString *)persistenceResponseString;
- (void)setKeysForPersistenceResponseString:(NSString *)str;
// method to begin fetching an access token, used by the sign-in object
- (GTMHTTPFetcher *)beginTokenFetchWithDelegate:(id)delegate
didFinishSelector:(SEL)finishedSel;
// Entry point to post a notification about a fetcher currently used for
// obtaining or refreshing a token; the sign-in object will also use this
// to indicate when the user's email address is being fetched.
//
// Fetch type constants are above under "notifications for token fetches"
- (void)notifyFetchIsRunning:(BOOL)isStarting
fetcher:(GTMHTTPFetcher *)fetcher
type:(NSString *)fetchType;
// Arbitrary key-value properties retained for the user
- (void)setProperty:(id)obj forKey:(NSString *)key;
- (id)propertyForKey:(NSString *)key;
//
// Utilities
//
+ (NSString *)encodedOAuthValueForString:(NSString *)str;
+ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict;
+ (NSDictionary *)dictionaryWithResponseString:(NSString *)responseStr;
+ (NSString *)scopeWithStrings:(NSString *)firsStr, ... NS_REQUIRES_NIL_TERMINATION;
@end
#endif // GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES

1166
OAuth 2/GTMOAuth2Authentication.m Executable file

File diff suppressed because it is too large Load Diff

184
OAuth 2/GTMOAuth2SignIn.h Executable file
View File

@@ -0,0 +1,184 @@
/* 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.
*/
//
// This sign-in object opens and closes the web view window as needed for
// users to sign in. For signing in to Google, it also obtains
// the authenticated user's email address.
//
// Typically, this will be managed for the application by
// GTMOAuth2ViewControllerTouch or GTMOAuth2WindowController, so this
// class's interface is interesting only if
// you are creating your own window controller for sign-in.
//
//
// Delegate methods implemented by the window controller
//
// The window controller implements two methods for use by the sign-in object,
// the webRequestSelector and the finishedSelector:
//
// webRequestSelector has a signature matching
// - (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request
//
// The web request selector will be invoked with a request to be displayed, or
// nil to close the window when the final callback request has been encountered.
//
//
// finishedSelector has a signature matching
// - (void)signin:(GTMOAuth2SignIn *)signin finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error
//
// The finished selector will be invoked when sign-in has completed, except
// when explicitly canceled by calling cancelSigningIn
//
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>
// GTMHTTPFetcher brings in GTLDefines/GDataDefines
#import "GTMHTTPFetcher.h"
#import "GTMOAuth2Authentication.h"
@class GTMOAuth2SignIn;
@interface GTMOAuth2SignIn : NSObject {
@private
GTMOAuth2Authentication *auth_;
// the endpoint for displaying the sign-in page
NSURL *authorizationURL_;
NSDictionary *additionalAuthorizationParameters_;
id delegate_;
SEL webRequestSelector_;
SEL finishedSelector_;
BOOL hasHandledCallback_;
GTMHTTPFetcher *pendingFetcher_;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
BOOL shouldFetchGoogleUserEmail_;
BOOL shouldFetchGoogleUserProfile_;
NSDictionary *userProfile_;
#endif
SCNetworkReachabilityRef reachabilityRef_;
NSTimer *networkLossTimer_;
NSTimeInterval networkLossTimeoutInterval_;
BOOL hasNotifiedNetworkLoss_;
id userData_;
}
@property (nonatomic, retain) GTMOAuth2Authentication *authentication;
@property (nonatomic, retain) NSURL *authorizationURL;
@property (nonatomic, retain) NSDictionary *additionalAuthorizationParameters;
// The delegate is released when signing in finishes or is cancelled
@property (nonatomic, retain) id delegate;
@property (nonatomic, assign) SEL webRequestSelector;
@property (nonatomic, assign) SEL finishedSelector;
@property (nonatomic, retain) id userData;
// By default, signing in to Google will fetch the user's email, but will not
// fetch the user's profile.
//
// The email is saved in the auth object.
// The profile is available immediately after sign-in.
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
@property (nonatomic, assign) BOOL shouldFetchGoogleUserEmail;
@property (nonatomic, assign) BOOL shouldFetchGoogleUserProfile;
@property (nonatomic, retain, readonly) NSDictionary *userProfile;
#endif
// The default timeout for an unreachable network during display of the
// sign-in page is 30 seconds; set this to 0 to have no timeout
@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval;
// The delegate is retained until sign-in has completed or been canceled
//
// designated initializer
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
delegate:(id)delegate
webRequestSelector:(SEL)webRequestSelector
finishedSelector:(SEL)finishedSelector;
// A default authentication object for signing in to Google services
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (GTMOAuth2Authentication *)standardGoogleAuthenticationForScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret;
#endif
#pragma mark Methods used by the Window Controller
// Start the sequence of fetches and sign-in window display for sign-in
- (BOOL)startSigningIn;
// Stop any pending fetches, and close the window (but don't call the
// delegate's finishedSelector)
- (void)cancelSigningIn;
// Window controllers must tell the sign-in object about any redirect
// requested by the web view, and any changes in the webview window title
//
// If these return YES then the event was handled by the
// sign-in object (typically by closing the window) and should be ignored by
// the window controller's web view
- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest;
- (BOOL)titleChanged:(NSString *)title;
- (BOOL)cookiesChanged:(NSHTTPCookieStorage *)cookieStorage;
- (BOOL)loadFailedWithError:(NSError *)error;
// Window controllers must tell the sign-in object if the window was closed
// prematurely by the user (but not by the sign-in object); this calls the
// delegate's finishedSelector
- (void)windowWasClosed;
#pragma mark -
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
// Revocation of an authorized token from Google
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth;
// Create a fetcher for obtaining the user's Google email address or profile,
// according to the current auth scopes.
//
// The auth object must have been created with appropriate scopes.
//
// The fetcher's response data can be parsed with NSJSONSerialization.
+ (GTMHTTPFetcher *)userInfoFetcherWithAuth:(GTMOAuth2Authentication *)auth;
#endif
#pragma mark -
// Standard authentication values
+ (NSString *)nativeClientRedirectURI;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (NSURL *)googleAuthorizationURL;
+ (NSURL *)googleTokenURL;
+ (NSURL *)googleUserInfoURL;
#endif
@end
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES

814
OAuth 2/GTMOAuth2SignIn.m Executable file
View File

@@ -0,0 +1,814 @@
/* 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.
*/
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
#define GTMOAUTH2SIGNIN_DEFINE_GLOBALS 1
#import "GTMOAuth2SignIn.h"
// we'll default to timing out if the network becomes unreachable for more
// than 30 seconds when the sign-in page is displayed
static const NSTimeInterval kDefaultNetworkLossTimeoutInterval = 30.0;
// URI indicating an installed app is signing in. This is described at
//
// http://code.google.com/apis/accounts/docs/OAuth2.html#IA
//
NSString *const kOOBString = @"urn:ietf:wg:oauth:2.0:oob";
@interface GTMOAuth2Authentication (InternalMethods)
- (NSDictionary *)dictionaryWithJSONData:(NSData *)data;
@end
@interface GTMOAuth2SignIn ()
@property (assign) BOOL hasHandledCallback;
@property (retain) GTMHTTPFetcher *pendingFetcher;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
@property (nonatomic, retain, readwrite) NSDictionary *userProfile;
#endif
- (void)invokeFinalCallbackWithError:(NSError *)error;
- (BOOL)startWebRequest;
+ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL
paramString:(NSString *)paramStr;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
- (void)fetchGoogleUserInfo;
#endif
- (void)finishSignInWithError:(NSError *)error;
- (void)handleCallbackReached;
- (void)auth:(GTMOAuth2Authentication *)auth
finishedWithFetcher:(GTMHTTPFetcher *)fetcher
error:(NSError *)error;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
- (void)infoFetcher:(GTMHTTPFetcher *)fetcher
finishedWithData:(NSData *)data
error:(NSError *)error;
#endif
- (void)closeTheWindow;
- (void)startReachabilityCheck;
- (void)stopReachabilityCheck;
- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef
changedFlags:(SCNetworkConnectionFlags)flags;
- (void)reachabilityTimerFired:(NSTimer *)timer;
@end
@implementation GTMOAuth2SignIn
@synthesize authentication = auth_;
@synthesize authorizationURL = authorizationURL_;
@synthesize additionalAuthorizationParameters = additionalAuthorizationParameters_;
@synthesize delegate = delegate_;
@synthesize webRequestSelector = webRequestSelector_;
@synthesize finishedSelector = finishedSelector_;
@synthesize hasHandledCallback = hasHandledCallback_;
@synthesize pendingFetcher = pendingFetcher_;
@synthesize userData = userData_;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
@synthesize shouldFetchGoogleUserEmail = shouldFetchGoogleUserEmail_;
@synthesize shouldFetchGoogleUserProfile = shouldFetchGoogleUserProfile_;
@synthesize userProfile = userProfile_;
#endif
@synthesize networkLossTimeoutInterval = networkLossTimeoutInterval_;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (NSURL *)googleAuthorizationURL {
NSString *str = @"https://accounts.google.com/o/oauth2/auth";
return [NSURL URLWithString:str];
}
+ (NSURL *)googleTokenURL {
NSString *str = @"https://accounts.google.com/o/oauth2/token";
return [NSURL URLWithString:str];
}
+ (NSURL *)googleRevocationURL {
NSString *urlStr = @"https://accounts.google.com/o/oauth2/revoke";
return [NSURL URLWithString:urlStr];
}
+ (NSURL *)googleUserInfoURL {
NSString *urlStr = @"https://www.googleapis.com/oauth2/v1/userinfo";
return [NSURL URLWithString:urlStr];
}
#endif
+ (NSString *)nativeClientRedirectURI {
return kOOBString;
}
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (GTMOAuth2Authentication *)standardGoogleAuthenticationForScope:(NSString *)scope
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret {
NSString *redirectURI = [self nativeClientRedirectURI];
NSURL *tokenURL = [self googleTokenURL];
GTMOAuth2Authentication *auth;
auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
tokenURL:tokenURL
redirectURI:redirectURI
clientID:clientID
clientSecret:clientSecret];
auth.scope = scope;
return auth;
}
#endif
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
authorizationURL:(NSURL *)authorizationURL
delegate:(id)delegate
webRequestSelector:(SEL)webRequestSelector
finishedSelector:(SEL)finishedSelector {
// check the selectors on debug builds
GTMAssertSelectorNilOrImplementedWithArgs(delegate, webRequestSelector,
@encode(GTMOAuth2SignIn *), @encode(NSURLRequest *), 0);
GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector,
@encode(GTMOAuth2SignIn *), @encode(GTMOAuth2Authentication *),
@encode(NSError *), 0);
// designated initializer
self = [super init];
if (self) {
auth_ = [auth retain];
authorizationURL_ = [authorizationURL retain];
delegate_ = [delegate retain];
webRequestSelector_ = webRequestSelector;
finishedSelector_ = finishedSelector;
// for Google authentication, we want to automatically fetch user info
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
NSString *host = [authorizationURL host];
if ([host hasSuffix:@".google.com"]) {
shouldFetchGoogleUserEmail_ = YES;
}
#endif
// default timeout for a lost internet connection while the server
// UI is displayed is 30 seconds
networkLossTimeoutInterval_ = kDefaultNetworkLossTimeoutInterval;
}
return self;
}
- (void)dealloc {
[self stopReachabilityCheck];
[auth_ release];
[authorizationURL_ release];
[additionalAuthorizationParameters_ release];
[delegate_ release];
[pendingFetcher_ release];
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
[userProfile_ release];
#endif
[userData_ release];
[super dealloc];
}
#pragma mark Sign-in Sequence Methods
// stop any pending fetches, and close the window (but don't call the
// delegate's finishedSelector)
- (void)cancelSigningIn {
[self.pendingFetcher stopFetching];
self.pendingFetcher = nil;
[self.authentication stopAuthorization];
[self closeTheWindow];
[delegate_ autorelease];
delegate_ = nil;
}
//
// This is the entry point to begin the sequence
// - display the authentication web page, and monitor redirects
// - exchange the code for an access token and a refresh token
// - for Google sign-in, fetch the user's email address
// - tell the delegate we're finished
//
- (BOOL)startSigningIn {
// For signing in to Google, append the scope for obtaining the authenticated
// user email and profile, as appropriate
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
GTMOAuth2Authentication *auth = self.authentication;
if (self.shouldFetchGoogleUserEmail) {
NSString *const emailScope = @"https://www.googleapis.com/auth/userinfo.email";
NSString *scope = auth.scope;
if ([scope rangeOfString:emailScope].location == NSNotFound) {
scope = [GTMOAuth2Authentication scopeWithStrings:scope, emailScope, nil];
auth.scope = scope;
}
}
if (self.shouldFetchGoogleUserProfile) {
NSString *const profileScope = @"https://www.googleapis.com/auth/userinfo.profile";
NSString *scope = auth.scope;
if ([scope rangeOfString:profileScope].location == NSNotFound) {
scope = [GTMOAuth2Authentication scopeWithStrings:scope, profileScope, nil];
auth.scope = scope;
}
}
#endif
// start the authorization
return [self startWebRequest];
}
- (NSMutableDictionary *)parametersForWebRequest {
GTMOAuth2Authentication *auth = self.authentication;
NSString *clientID = auth.clientID;
NSString *redirectURI = auth.redirectURI;
BOOL hasClientID = ([clientID length] > 0);
BOOL hasRedirect = ([redirectURI length] > 0
|| redirectURI == [[self class] nativeClientRedirectURI]);
if (!hasClientID || !hasRedirect) {
#if DEBUG
NSAssert(hasClientID, @"GTMOAuth2SignIn: clientID needed");
NSAssert(hasRedirect, @"GTMOAuth2SignIn: redirectURI needed");
#endif
return NO;
}
// invoke the UI controller's web request selector to display
// the authorization page
// add params to the authorization URL
NSString *scope = auth.scope;
if ([scope length] == 0) scope = nil;
NSMutableDictionary *paramsDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
@"code", @"response_type",
clientID, @"client_id",
scope, @"scope", // scope may be nil
nil];
if (redirectURI) {
[paramsDict setObject:redirectURI forKey:@"redirect_uri"];
}
return paramsDict;
}
- (BOOL)startWebRequest {
NSMutableDictionary *paramsDict = [self parametersForWebRequest];
NSDictionary *additionalParams = self.additionalAuthorizationParameters;
if (additionalParams) {
[paramsDict addEntriesFromDictionary:additionalParams];
}
NSString *paramStr = [GTMOAuth2Authentication encodedQueryParametersForDictionary:paramsDict];
NSURL *authorizationURL = self.authorizationURL;
NSMutableURLRequest *request;
request = [[self class] mutableURLRequestWithURL:authorizationURL
paramString:paramStr];
[delegate_ performSelector:self.webRequestSelector
withObject:self
withObject:request];
// at this point, we're waiting on the server-driven html UI, so
// we want notification if we lose connectivity to the web server
[self startReachabilityCheck];
return YES;
}
// utility for making a request from an old URL with some additional parameters
+ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL
paramString:(NSString *)paramStr {
NSString *query = [oldURL query];
if ([query length] > 0) {
query = [query stringByAppendingFormat:@"&%@", paramStr];
} else {
query = paramStr;
}
NSString *portStr = @"";
NSString *oldPort = [[oldURL port] stringValue];
if ([oldPort length] > 0) {
portStr = [@":" stringByAppendingString:oldPort];
}
NSString *qMark = [query length] > 0 ? @"?" : @"";
NSString *newURLStr = [NSString stringWithFormat:@"%@://%@%@%@%@%@",
[oldURL scheme], [oldURL host], portStr,
[oldURL path], qMark, query];
NSURL *newURL = [NSURL URLWithString:newURLStr];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:newURL];
return request;
}
// entry point for the window controller to tell us that the window
// prematurely closed
- (void)windowWasClosed {
[self stopReachabilityCheck];
NSError *error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
code:kGTMOAuth2ErrorWindowClosed
userInfo:nil];
[self invokeFinalCallbackWithError:error];
}
// internal method to tell the window controller to close the window
- (void)closeTheWindow {
[self stopReachabilityCheck];
// a nil request means the window should be closed
[delegate_ performSelector:self.webRequestSelector
withObject:self
withObject:nil];
}
// entry point for the window controller to tell us what web page has been
// requested
//
// When the request is for the callback URL, this method invokes
// handleCallbackReached and returns YES
- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest {
// for Google's installed app sign-in protocol, we'll look for the
// end-of-sign-in indicator in the titleChanged: method below
NSString *redirectURI = self.authentication.redirectURI;
if (redirectURI == nil) return NO;
// when we're searching for the window title string, then we can ignore
// redirects
NSString *standardURI = [[self class] nativeClientRedirectURI];
if (standardURI != nil && [redirectURI isEqual:standardURI]) return NO;
// compare the redirectURI, which tells us when the web sign-in is done,
// to the actual redirection
NSURL *redirectURL = [NSURL URLWithString:redirectURI];
NSURL *requestURL = [redirectedRequest URL];
// avoid comparing to nil host and path values (such as when redirected to
// "about:blank")
NSString *requestHost = [requestURL host];
NSString *requestPath = [requestURL path];
BOOL isCallback;
if (requestHost && requestPath) {
isCallback = [[redirectURL host] isEqual:[requestURL host]]
&& [[redirectURL path] isEqual:[requestURL path]];
} else if (requestURL) {
// handle "about:blank"
isCallback = [redirectURL isEqual:requestURL];
} else {
isCallback = NO;
}
if (!isCallback) {
// tell the caller that this request is nothing interesting
return NO;
}
// we've reached the callback URL
// try to get the access code
if (!self.hasHandledCallback) {
NSString *responseStr = [[redirectedRequest URL] query];
[self.authentication setKeysForResponseString:responseStr];
#if DEBUG
NSAssert([self.authentication.code length] > 0
|| [self.authentication.errorString length] > 0,
@"response lacks auth code or error");
#endif
[self handleCallbackReached];
}
// tell the delegate that we did handle this request
return YES;
}
// entry point for the window controller to tell us when a new page title has
// been loadded
//
// When the title indicates sign-in has completed, this method invokes
// handleCallbackReached and returns YES
- (BOOL)titleChanged:(NSString *)title {
// return YES if the OAuth flow ending title was detected
// right now we're just looking for a parameter list following the last space
// in the title string, but hopefully we'll eventually get something better
// from the server to search for
NSRange paramsRange = [title rangeOfString:@" "
options:NSBackwardsSearch];
NSUInteger spaceIndex = paramsRange.location;
if (spaceIndex != NSNotFound) {
NSString *responseStr = [title substringFromIndex:(spaceIndex + 1)];
NSDictionary *dict = [GTMOAuth2Authentication dictionaryWithResponseString:responseStr];
NSString *code = [dict objectForKey:@"code"];
NSString *error = [dict objectForKey:@"error"];
if ([code length] > 0 || [error length] > 0) {
if (!self.hasHandledCallback) {
[self.authentication setKeysForResponseDictionary:dict];
[self handleCallbackReached];
}
return YES;
}
}
return NO;
}
- (BOOL)cookiesChanged:(NSHTTPCookieStorage *)cookieStorage {
// We're ignoring these.
return NO;
};
// entry point for the window controller to tell us when a load has failed
// in the webview
//
// if the initial authorization URL fails, bail out so the user doesn't
// see an empty webview
- (BOOL)loadFailedWithError:(NSError *)error {
NSURL *authorizationURL = self.authorizationURL;
NSURL *failedURL = [[error userInfo] valueForKey:@"NSErrorFailingURLKey"]; // NSURLErrorFailingURLErrorKey defined in 10.6
BOOL isAuthURL = [[failedURL host] isEqual:[authorizationURL host]]
&& [[failedURL path] isEqual:[authorizationURL path]];
if (isAuthURL) {
// We can assume that we have no pending fetchers, since we only
// handle failure to load the initial authorization URL
[self closeTheWindow];
[self invokeFinalCallbackWithError:error];
return YES;
}
return NO;
}
- (void)handleCallbackReached {
// the callback page was requested, or the authenticate code was loaded
// into a page's title, so exchange the auth code for access & refresh tokens
// and tell the window to close
// avoid duplicate signals that the callback point has been reached
self.hasHandledCallback = YES;
[self closeTheWindow];
NSError *error = nil;
GTMOAuth2Authentication *auth = self.authentication;
NSString *code = auth.code;
if ([code length] > 0) {
// exchange the code for a token
SEL sel = @selector(auth:finishedWithFetcher:error:);
GTMHTTPFetcher *fetcher = [auth beginTokenFetchWithDelegate:self
didFinishSelector:sel];
if (fetcher == nil) {
error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
code:-1
userInfo:nil];
} else {
self.pendingFetcher = fetcher;
}
// notify the app so it can put up a post-sign in, pre-token exchange UI
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:kGTMOAuth2UserSignedIn
object:self
userInfo:nil];
} else {
// the callback lacked an auth code
NSString *errStr = auth.errorString;
NSDictionary *userInfo = nil;
if ([errStr length] > 0) {
userInfo = [NSDictionary dictionaryWithObject:errStr
forKey:kGTMOAuth2ErrorMessageKey];
}
error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
code:kGTMOAuth2ErrorAuthorizationFailed
userInfo:userInfo];
}
if (error) {
[self finishSignInWithError:error];
}
}
- (void)auth:(GTMOAuth2Authentication *)auth
finishedWithFetcher:(GTMHTTPFetcher *)fetcher
error:(NSError *)error {
self.pendingFetcher = nil;
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
if (error == nil
&& (self.shouldFetchGoogleUserEmail || self.shouldFetchGoogleUserProfile)
&& [self.authentication.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) {
// fetch the user's information from the Google server
[self fetchGoogleUserInfo];
} else {
// we're not authorizing with Google, so we're done
[self finishSignInWithError:error];
}
#else
[self finishSignInWithError:error];
#endif
}
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (GTMHTTPFetcher *)userInfoFetcherWithAuth:(GTMOAuth2Authentication *)auth {
// create a fetcher for obtaining the user's email or profile
NSURL *infoURL = [[self class] googleUserInfoURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:infoURL];
NSString *userAgent = [auth userAgent];
[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
[request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
GTMHTTPFetcher *fetcher;
id <GTMHTTPFetcherServiceProtocol> fetcherService = auth.fetcherService;
if (fetcherService) {
fetcher = [fetcherService fetcherWithRequest:request];
} else {
fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
}
fetcher.authorizer = auth;
fetcher.retryEnabled = YES;
fetcher.maxRetryInterval = 15.0;
fetcher.comment = @"user info";
return fetcher;
}
- (void)fetchGoogleUserInfo {
// fetch the user's email address or profile
GTMOAuth2Authentication *auth = self.authentication;
GTMHTTPFetcher *fetcher = [[self class] userInfoFetcherWithAuth:auth];
[fetcher beginFetchWithDelegate:self
didFinishSelector:@selector(infoFetcher:finishedWithData:error:)];
self.pendingFetcher = fetcher;
[auth notifyFetchIsRunning:YES
fetcher:fetcher
type:kGTMOAuth2FetchTypeUserInfo];
}
- (void)infoFetcher:(GTMHTTPFetcher *)fetcher
finishedWithData:(NSData *)data
error:(NSError *)error {
GTMOAuth2Authentication *auth = self.authentication;
[auth notifyFetchIsRunning:NO
fetcher:fetcher
type:nil];
self.pendingFetcher = nil;
if (error) {
#if DEBUG
if (data) {
NSString *dataStr = [[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding] autorelease];
NSLog(@"infoFetcher error: %@\n%@", error, dataStr);
}
#endif
} else {
// We have the authenticated user's info
if (data) {
NSDictionary *profileDict = [auth dictionaryWithJSONData:data];
if (profileDict) {
self.userProfile = profileDict;
// Save the email into the auth object
NSString *email = [profileDict objectForKey:@"email"];
[auth setUserEmail:email];
NSNumber *verified = [profileDict objectForKey:@"verified_email"];
[auth setUserEmailIsVerified:[verified stringValue]];
}
}
}
[self finishSignInWithError:error];
}
#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
- (void)finishSignInWithError:(NSError *)error {
[self invokeFinalCallbackWithError:error];
}
// convenience method for making the final call to our delegate
- (void)invokeFinalCallbackWithError:(NSError *)error {
if (delegate_ && finishedSelector_) {
GTMOAuth2Authentication *auth = self.authentication;
NSMethodSignature *sig = [delegate_ methodSignatureForSelector:finishedSelector_];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setSelector:finishedSelector_];
[invocation setTarget:delegate_];
[invocation setArgument:&self atIndex:2];
[invocation setArgument:&auth atIndex:3];
[invocation setArgument:&error atIndex:4];
[invocation invoke];
}
// we'll no longer send messages to the delegate
//
// we want to autorelease it rather than assign to the property in case
// the delegate is below us in the call stack
[delegate_ autorelease];
delegate_ = nil;
}
#pragma mark Reachability monitoring
static void ReachabilityCallBack(SCNetworkReachabilityRef target,
SCNetworkConnectionFlags flags,
void *info) {
// pass the flags to the signIn object
GTMOAuth2SignIn *signIn = (GTMOAuth2SignIn *)info;
[signIn reachabilityTarget:target
changedFlags:flags];
}
- (void)startReachabilityCheck {
// the user may set the timeout to 0 to skip the reachability checking
// during display of the sign-in page
if (networkLossTimeoutInterval_ <= 0.0 || reachabilityRef_ != NULL) {
return;
}
// create a reachability target from the authorization URL, add our callback,
// and schedule it on the run loop so we'll be notified if the network drops
NSURL *url = self.authorizationURL;
const char* host = [[url host] UTF8String];
reachabilityRef_ = SCNetworkReachabilityCreateWithName(kCFAllocatorSystemDefault,
host);
if (reachabilityRef_) {
BOOL isScheduled = NO;
SCNetworkReachabilityContext ctx = { 0, self, NULL, NULL, NULL };
if (SCNetworkReachabilitySetCallback(reachabilityRef_,
ReachabilityCallBack, &ctx)) {
if (SCNetworkReachabilityScheduleWithRunLoop(reachabilityRef_,
CFRunLoopGetCurrent(),
kCFRunLoopDefaultMode)) {
isScheduled = YES;
}
}
if (!isScheduled) {
CFRelease(reachabilityRef_);
reachabilityRef_ = NULL;
}
}
}
- (void)destroyUnreachabilityTimer {
[networkLossTimer_ invalidate];
[networkLossTimer_ autorelease];
networkLossTimer_ = nil;
}
- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef
changedFlags:(SCNetworkConnectionFlags)flags {
BOOL isConnected = (flags & kSCNetworkFlagsReachable) != 0
&& (flags & kSCNetworkFlagsConnectionRequired) == 0;
if (isConnected) {
// server is again reachable
[self destroyUnreachabilityTimer];
if (hasNotifiedNetworkLoss_) {
// tell the user that the network has been found
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:kGTMOAuth2NetworkFound
object:self
userInfo:nil];
hasNotifiedNetworkLoss_ = NO;
}
} else {
// the server has become unreachable; start the timer, if necessary
if (networkLossTimer_ == nil
&& networkLossTimeoutInterval_ > 0
&& !hasNotifiedNetworkLoss_) {
SEL sel = @selector(reachabilityTimerFired:);
networkLossTimer_ = [[NSTimer scheduledTimerWithTimeInterval:networkLossTimeoutInterval_
target:self
selector:sel
userInfo:nil
repeats:NO] retain];
}
}
}
- (void)reachabilityTimerFired:(NSTimer *)timer {
// the user may call [[notification object] cancelSigningIn] to
// dismiss the sign-in
if (!hasNotifiedNetworkLoss_) {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:kGTMOAuth2NetworkLost
object:self
userInfo:nil];
hasNotifiedNetworkLoss_ = YES;
}
[self destroyUnreachabilityTimer];
}
- (void)stopReachabilityCheck {
[self destroyUnreachabilityTimer];
if (reachabilityRef_) {
SCNetworkReachabilityUnscheduleFromRunLoop(reachabilityRef_,
CFRunLoopGetCurrent(),
kCFRunLoopDefaultMode);
SCNetworkReachabilitySetCallback(reachabilityRef_, NULL, NULL);
CFRelease(reachabilityRef_);
reachabilityRef_ = NULL;
}
}
#pragma mark Token Revocation
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
if (auth.canAuthorize
&& [auth.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) {
// create a signed revocation request for this authentication object
NSURL *url = [self googleRevocationURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
NSString *token = auth.refreshToken;
NSString *encoded = [GTMOAuth2Authentication encodedOAuthValueForString:token];
NSString *body = [@"token=" stringByAppendingString:encoded];
[request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
[request setHTTPMethod:@"POST"];
NSString *userAgent = [auth userAgent];
[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
// there's nothing to be done if revocation succeeds or fails
GTMHTTPFetcher *fetcher;
id <GTMHTTPFetcherServiceProtocol> fetcherService = auth.fetcherService;
if (fetcherService) {
fetcher = [fetcherService fetcherWithRequest:request];
} else {
fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
}
fetcher.comment = @"revoke token";
// Use a completion handler fetch for better debugging, but only if we're
// guaranteed that blocks are available in the runtime
#if (!TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MIN_REQUIRED >= 1060)) || \
(TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000))
// Blocks are available
[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
#if DEBUG
if (error) {
NSString *errStr = [[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding] autorelease];
NSLog(@"revoke error: %@", errStr);
}
#endif // DEBUG
}];
#else
// Blocks may not be available
[fetcher beginFetchWithDelegate:nil didFinishSelector:NULL];
#endif
}
[auth reset];
}
#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
@end
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES

View File

@@ -0,0 +1,333 @@
/* 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;
- (IBAction)toggleStorePasswordInKeychain:(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

View File

@@ -0,0 +1,738 @@
/* 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 *htmlTemplate = @"<html><body><div align=center><font size='7'>"
@"&#x231A; ?<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];
}
- (IBAction)toggleStorePasswordInKeychain:(id)sender {
if ([sender state] == NSOffState) {
NSBeginAlertSheet(NSLocalizedString(@"Do you want to disable saving your login data in your keychain?", nil), NSLocalizedString(@"Keep enabled", nil), NSLocalizedString(@"Disable", nil), nil, self.window, self, @selector(disableKeychainSheetDidEnd:returnCode:contextInfo:), nil, NULL, NSLocalizedString(@"If you do not save the password you will have to sign in every time you use this feature.", nil));
}
}
- (void)disableKeychainSheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
if (returnCode == NSAlertDefaultReturn) {
self.keychainCheckbox.state = NSOnState;
}
}
#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 (but not immediately, in case
// we're called in response to some window event)
NSWindow *parentWindow = self.sheetModalForWindow;
if (parentWindow) {
[NSApp endSheet:[self window]];
} else {
[[self window] performSelector:@selector(close)
withObject:nil
afterDelay:0.1
inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
// Avoid more callbacks after the delayed close happens, as the window
// controller may be gone.
[[self webView] setResourceLoadDelegate:nil];
}
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

13
OAuth 2/OAuth2.h Executable file
View File

@@ -0,0 +1,13 @@
//
// NSObject_OAuth2.h
// Notifications for YouTube
//
// Created by Kim Wittenburg on 22.09.12.
// Copyright (c) 2012 Kim Wittenburg. All rights reserved.
//
#import "GTMHTTPFetcher.h"
#import "GTMHTTPFetchHistory.h"
#import "GTMOAuth2Authentication.h"
#import "GTMOAuth2SignIn.h"
#import "GTMOAuth2WindowController.h"

View File

@@ -0,0 +1,568 @@
<?xml version="1.0" encoding="UTF-8"?>
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">12E55</string>
<string key="IBDocument.InterfaceBuilderVersion">3084</string>
<string key="IBDocument.AppKitVersion">1187.39</string>
<string key="IBDocument.HIToolboxVersion">626.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>3084</string>
<string>2053</string>
</object>
</object>
<object class="NSArray" key="IBDocument.IntegratedClassDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSButton</string>
<string>NSButtonCell</string>
<string>NSCustomObject</string>
<string>NSUserDefaultsController</string>
<string>NSView</string>
<string>NSWindowTemplate</string>
<string>WebView</string>
</object>
<object class="NSArray" key="IBDocument.PluginDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
<object class="NSMutableDictionary" key="IBDocument.Metadata">
<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
<integer value="1" key="NS.object.0"/>
</object>
<object class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSCustomObject" id="1001">
<string key="NSClassName">GTMOAuth2WindowController</string>
</object>
<object class="NSCustomObject" id="1003">
<string key="NSClassName">FirstResponder</string>
</object>
<object class="NSCustomObject" id="1004">
<string key="NSClassName">NSApplication</string>
</object>
<object class="NSWindowTemplate" id="996099970">
<int key="NSWindowStyleMask">11</int>
<int key="NSWindowBacking">2</int>
<string key="NSWindowRect">{{522, 328}, {515, 419}}</string>
<int key="NSWTFlags">536870912</int>
<string key="NSWindowTitle">Anmelden</string>
<string key="NSWindowClass">NSWindow</string>
<nil key="NSViewClass"/>
<nil key="NSUserInterfaceItemIdentifier"/>
<string key="NSWindowContentMinSize">{475, 290}</string>
<object class="NSView" key="NSWindowView" id="563959409">
<reference key="NSNextResponder"/>
<int key="NSvFlags">256</int>
<object class="NSMutableArray" key="NSSubviews">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="WebView" id="697605106">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">274</int>
<object class="NSMutableSet" key="NSDragTypes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="set.sortedObjects">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>Apple HTML pasteboard type</string>
<string>Apple PDF pasteboard type</string>
<string>Apple PICT pasteboard type</string>
<string>Apple URL pasteboard type</string>
<string>Apple Web Archive pasteboard type</string>
<string>NSColor pasteboard type</string>
<string>NSFilenamesPboardType</string>
<string>NSStringPboardType</string>
<string>NeXT RTFD pasteboard type</string>
<string>NeXT Rich Text Format v1.0 pasteboard type</string>
<string>NeXT TIFF v4.0 pasteboard type</string>
<string>WebURLsWithTitlesPboardType</string>
<string>public.png</string>
<string>public.url</string>
<string>public.url-name</string>
</object>
</object>
<string key="NSFrame">{{0, 20}, {515, 399}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSNextKeyView"/>
<string key="FrameName"/>
<string key="GroupName"/>
<object class="WebPreferences" key="Preferences">
<string key="Identifier"/>
<object class="NSMutableDictionary" key="Values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>WebKitDefaultFixedFontSize</string>
<string>WebKitDefaultFontSize</string>
<string>WebKitMinimumFontSize</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="12"/>
<integer value="12"/>
<integer value="1"/>
</object>
</object>
</object>
<bool key="UseBackForwardList">YES</bool>
<bool key="AllowsUndo">YES</bool>
</object>
<object class="NSButton" id="736908656">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{479, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="215388286">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<object class="NSFont" key="NSSupport" id="895134484">
<string key="NSName">LucidaGrande</string>
<double key="NSSize">13</double>
<int key="NSfFlags">1044</int>
</object>
<reference key="NSControlView" ref="736908656"/>
<int key="NSButtonFlags">-2041823232</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSStopProgressTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string type="base64-UTF8" key="NSKeyEquivalent">Gw</string>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="771759786">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{437, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSNextKeyView" ref="36322049"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="656055052">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<reference key="NSSupport" ref="895134484"/>
<reference key="NSControlView" ref="771759786"/>
<int key="NSButtonFlags">-2041823232</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSGoLeftTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="36322049">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{456, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSNextKeyView" ref="736908656"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="282674264">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<reference key="NSSupport" ref="895134484"/>
<reference key="NSControlView" ref="36322049"/>
<int key="NSButtonFlags">-2042347520</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSGoRightTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="823054555">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">292</int>
<string key="NSFrame">{{2, 1}, {429, 18}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSNextKeyView" ref="771759786"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="817997953">
<int key="NSCellFlags">-2080374784</int>
<int key="NSCellFlags2">131072</int>
<string key="NSContents">Kennwort in meinem Schlüsselbund sichern</string>
<object class="NSFont" key="NSSupport">
<string key="NSName">LucidaGrande</string>
<double key="NSSize">11</double>
<int key="NSfFlags">3100</int>
</object>
<reference key="NSControlView" ref="823054555"/>
<int key="NSButtonFlags">1211912448</int>
<int key="NSButtonFlags2">2</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSSwitch</string>
</object>
<object class="NSButtonImageSource" key="NSAlternateImage">
<string key="NSImageName">NSSwitch</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">200</int>
<int key="NSPeriodicInterval">25</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
</object>
<string key="NSFrameSize">{515, 419}</string>
<reference key="NSSuperview"/>
<reference key="NSNextKeyView" ref="697605106"/>
</object>
<string key="NSScreenRect">{{0, 0}, {1680, 1028}}</string>
<string key="NSMinSize">{475, 312}</string>
<string key="NSMaxSize">{10000000000000, 10000000000000}</string>
<bool key="NSWindowIsRestorable">YES</bool>
</object>
<object class="NSUserDefaultsController" id="681499023">
<bool key="NSSharedInstance">YES</bool>
</object>
</object>
<object class="IBObjectContainer" key="IBDocument.Objects">
<object class="NSMutableArray" key="connectionRecords">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">window</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="996099970"/>
</object>
<int key="connectionID">8</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">closeWindow:</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="736908656"/>
</object>
<int key="connectionID">42</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">keychainCheckbox</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="823054555"/>
</object>
<int key="connectionID">46</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webBackButton</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="771759786"/>
</object>
<int key="connectionID">47</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webCloseButton</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="736908656"/>
</object>
<int key="connectionID">48</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webView</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="697605106"/>
</object>
<int key="connectionID">49</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">toggleStorePasswordInKeychain:</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="823054555"/>
</object>
<int key="connectionID">50</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">delegate</string>
<reference key="source" ref="996099970"/>
<reference key="destination" ref="1001"/>
</object>
<int key="connectionID">7</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">goBack:</string>
<reference key="source" ref="697605106"/>
<reference key="destination" ref="771759786"/>
</object>
<int key="connectionID">28</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">goForward:</string>
<reference key="source" ref="697605106"/>
<reference key="destination" ref="36322049"/>
</object>
<int key="connectionID">29</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">enabled: webView.canGoBack</string>
<reference key="source" ref="771759786"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="771759786"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">enabled: webView.canGoBack</string>
<string key="NSBinding">enabled</string>
<string key="NSKeyPath">webView.canGoBack</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">31</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">enabled: webView.canGoForward</string>
<reference key="source" ref="36322049"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="36322049"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">enabled: webView.canGoForward</string>
<string key="NSBinding">enabled</string>
<string key="NSKeyPath">webView.canGoForward</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">35</int>
</object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBObjectRecord">
<int key="objectID">0</int>
<object class="NSArray" key="object" id="0">
<bool key="EncodedWithXMLCoder">YES</bool>
</object>
<reference key="children" ref="1000"/>
<nil key="parent"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">-2</int>
<reference key="object" ref="1001"/>
<reference key="parent" ref="0"/>
<string key="objectName">File's Owner</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-1</int>
<reference key="object" ref="1003"/>
<reference key="parent" ref="0"/>
<string key="objectName">First Responder</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-3</int>
<reference key="object" ref="1004"/>
<reference key="parent" ref="0"/>
<string key="objectName">Application</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">3</int>
<reference key="object" ref="996099970"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="563959409"/>
</object>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">4</int>
<reference key="object" ref="563959409"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="697605106"/>
<reference ref="823054555"/>
<reference ref="36322049"/>
<reference ref="771759786"/>
<reference ref="736908656"/>
</object>
<reference key="parent" ref="996099970"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">5</int>
<reference key="object" ref="697605106"/>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">17</int>
<reference key="object" ref="736908656"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="215388286"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">18</int>
<reference key="object" ref="215388286"/>
<reference key="parent" ref="736908656"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">19</int>
<reference key="object" ref="771759786"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="656055052"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">20</int>
<reference key="object" ref="656055052"/>
<reference key="parent" ref="771759786"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">26</int>
<reference key="object" ref="36322049"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="282674264"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">27</int>
<reference key="object" ref="282674264"/>
<reference key="parent" ref="36322049"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">32</int>
<reference key="object" ref="681499023"/>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">43</int>
<reference key="object" ref="823054555"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="817997953"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">44</int>
<reference key="object" ref="817997953"/>
<reference key="parent" ref="823054555"/>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="flattenedProperties">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>-1.IBPluginDependency</string>
<string>-2.IBPluginDependency</string>
<string>-3.IBPluginDependency</string>
<string>17.IBPluginDependency</string>
<string>18.IBPluginDependency</string>
<string>19.IBPluginDependency</string>
<string>20.IBPluginDependency</string>
<string>26.IBPluginDependency</string>
<string>27.IBPluginDependency</string>
<string>3.IBPluginDependency</string>
<string>3.IBWindowTemplateEditedContentRect</string>
<string>3.NSWindowTemplate.visibleAtLaunch</string>
<string>32.IBPluginDependency</string>
<string>4.IBPluginDependency</string>
<string>43.IBPluginDependency</string>
<string>44.IBPluginDependency</string>
<string>5.IBPluginDependency</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>{{112, 709}, {515, 419}}</string>
<boolean value="NO"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
</object>
<object class="NSMutableDictionary" key="unlocalizedProperties">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference key="dict.sortedKeys" ref="0"/>
<reference key="dict.values" ref="0"/>
</object>
<nil key="activeLocalization"/>
<object class="NSMutableDictionary" key="localizations">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference key="dict.sortedKeys" ref="0"/>
<reference key="dict.values" ref="0"/>
</object>
<nil key="sourceID"/>
<int key="maxID">50</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes"/>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>
</object>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string>
<integer value="3000" key="NS.object.0"/>
</object>
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSGoLeftTemplate</string>
<string>NSGoRightTemplate</string>
<string>NSStopProgressTemplate</string>
<string>NSSwitch</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>{9, 9}</string>
<string>{9, 9}</string>
<string>{11, 11}</string>
<string>{15, 15}</string>
</object>
</object>
</data>
</archive>

View File

@@ -0,0 +1,664 @@
<?xml version="1.0" encoding="UTF-8"?>
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">12E55</string>
<string key="IBDocument.InterfaceBuilderVersion">3084</string>
<string key="IBDocument.AppKitVersion">1187.39</string>
<string key="IBDocument.HIToolboxVersion">626.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>3084</string>
<string>2053</string>
</object>
</object>
<object class="NSArray" key="IBDocument.IntegratedClassDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSButton</string>
<string>NSButtonCell</string>
<string>NSCustomObject</string>
<string>NSUserDefaultsController</string>
<string>NSView</string>
<string>NSWindowTemplate</string>
<string>WebView</string>
</object>
<object class="NSArray" key="IBDocument.PluginDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
<object class="NSMutableDictionary" key="IBDocument.Metadata">
<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
<integer value="1" key="NS.object.0"/>
</object>
<object class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSCustomObject" id="1001">
<string key="NSClassName">GTMOAuth2WindowController</string>
</object>
<object class="NSCustomObject" id="1003">
<string key="NSClassName">FirstResponder</string>
</object>
<object class="NSCustomObject" id="1004">
<string key="NSClassName">NSApplication</string>
</object>
<object class="NSWindowTemplate" id="996099970">
<int key="NSWindowStyleMask">11</int>
<int key="NSWindowBacking">2</int>
<string key="NSWindowRect">{{558, 328}, {515, 419}}</string>
<int key="NSWTFlags">536870912</int>
<string key="NSWindowTitle">Sign In</string>
<string key="NSWindowClass">NSWindow</string>
<nil key="NSViewClass"/>
<nil key="NSUserInterfaceItemIdentifier"/>
<string key="NSWindowContentMinSize">{475, 290}</string>
<object class="NSView" key="NSWindowView" id="563959409">
<reference key="NSNextResponder"/>
<int key="NSvFlags">256</int>
<object class="NSMutableArray" key="NSSubviews">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="WebView" id="697605106">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">274</int>
<object class="NSMutableSet" key="NSDragTypes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="set.sortedObjects">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>Apple HTML pasteboard type</string>
<string>Apple PDF pasteboard type</string>
<string>Apple PICT pasteboard type</string>
<string>Apple URL pasteboard type</string>
<string>Apple Web Archive pasteboard type</string>
<string>NSColor pasteboard type</string>
<string>NSFilenamesPboardType</string>
<string>NSStringPboardType</string>
<string>NeXT RTFD pasteboard type</string>
<string>NeXT Rich Text Format v1.0 pasteboard type</string>
<string>NeXT TIFF v4.0 pasteboard type</string>
<string>WebURLsWithTitlesPboardType</string>
<string>public.png</string>
<string>public.url</string>
<string>public.url-name</string>
</object>
</object>
<string key="NSFrame">{{0, 20}, {515, 399}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView"/>
<string key="FrameName"/>
<string key="GroupName"/>
<object class="WebPreferences" key="Preferences">
<string key="Identifier"/>
<object class="NSMutableDictionary" key="Values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>WebKitDefaultFixedFontSize</string>
<string>WebKitDefaultFontSize</string>
<string>WebKitMinimumFontSize</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="12"/>
<integer value="12"/>
<integer value="1"/>
</object>
</object>
</object>
<bool key="UseBackForwardList">YES</bool>
<bool key="AllowsUndo">YES</bool>
</object>
<object class="NSButton" id="736908656">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{479, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSWindow"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="215388286">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<object class="NSFont" key="NSSupport" id="895134484">
<string key="NSName">LucidaGrande</string>
<double key="NSSize">13</double>
<int key="NSfFlags">1044</int>
</object>
<reference key="NSControlView" ref="736908656"/>
<int key="NSButtonFlags">-2041823232</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSStopProgressTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string type="base64-UTF8" key="NSKeyEquivalent">Gw</string>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="771759786">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{437, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="36322049"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="656055052">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<reference key="NSSupport" ref="895134484"/>
<reference key="NSControlView" ref="771759786"/>
<int key="NSButtonFlags">-2041823232</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSGoLeftTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="36322049">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">289</int>
<string key="NSFrame">{{456, 0}, {16, 19}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="736908656"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="282674264">
<int key="NSCellFlags">67108864</int>
<int key="NSCellFlags2">134217728</int>
<string key="NSContents"/>
<reference key="NSSupport" ref="895134484"/>
<reference key="NSControlView" ref="36322049"/>
<int key="NSButtonFlags">-2042347520</int>
<int key="NSButtonFlags2">134</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSGoRightTemplate</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
<object class="NSButton" id="823054555">
<reference key="NSNextResponder" ref="563959409"/>
<int key="NSvFlags">292</int>
<string key="NSFrame">{{2, 1}, {429, 18}}</string>
<reference key="NSSuperview" ref="563959409"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="771759786"/>
<bool key="NSEnabled">YES</bool>
<object class="NSButtonCell" key="NSCell" id="817997953">
<int key="NSCellFlags">-2080374784</int>
<int key="NSCellFlags2">131072</int>
<string key="NSContents">Save password in my keychain</string>
<object class="NSFont" key="NSSupport">
<string key="NSName">LucidaGrande</string>
<double key="NSSize">11</double>
<int key="NSfFlags">3100</int>
</object>
<reference key="NSControlView" ref="823054555"/>
<int key="NSButtonFlags">1211912448</int>
<int key="NSButtonFlags2">2</int>
<object class="NSCustomResource" key="NSNormalImage">
<string key="NSClassName">NSImage</string>
<string key="NSResourceName">NSSwitch</string>
</object>
<object class="NSButtonImageSource" key="NSAlternateImage">
<string key="NSImageName">NSSwitch</string>
</object>
<string key="NSAlternateContents"/>
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">200</int>
<int key="NSPeriodicInterval">25</int>
</object>
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
</object>
</object>
<string key="NSFrameSize">{515, 419}</string>
<reference key="NSSuperview"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="697605106"/>
</object>
<string key="NSScreenRect">{{0, 0}, {1680, 1028}}</string>
<string key="NSMinSize">{475, 312}</string>
<string key="NSMaxSize">{10000000000000, 10000000000000}</string>
<bool key="NSWindowIsRestorable">YES</bool>
</object>
<object class="NSUserDefaultsController" id="681499023">
<bool key="NSSharedInstance">YES</bool>
</object>
</object>
<object class="IBObjectContainer" key="IBDocument.Objects">
<object class="NSMutableArray" key="connectionRecords">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">window</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="996099970"/>
</object>
<int key="connectionID">8</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">closeWindow:</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="736908656"/>
</object>
<int key="connectionID">42</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">keychainCheckbox</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="823054555"/>
</object>
<int key="connectionID">46</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webBackButton</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="771759786"/>
</object>
<int key="connectionID">47</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webCloseButton</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="736908656"/>
</object>
<int key="connectionID">48</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webView</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="697605106"/>
</object>
<int key="connectionID">49</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">toggleStorePasswordInKeychain:</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="823054555"/>
</object>
<int key="connectionID">50</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">delegate</string>
<reference key="source" ref="996099970"/>
<reference key="destination" ref="1001"/>
</object>
<int key="connectionID">7</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">goBack:</string>
<reference key="source" ref="697605106"/>
<reference key="destination" ref="771759786"/>
</object>
<int key="connectionID">28</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">goForward:</string>
<reference key="source" ref="697605106"/>
<reference key="destination" ref="36322049"/>
</object>
<int key="connectionID">29</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">enabled: webView.canGoBack</string>
<reference key="source" ref="771759786"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="771759786"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">enabled: webView.canGoBack</string>
<string key="NSBinding">enabled</string>
<string key="NSKeyPath">webView.canGoBack</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">31</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">enabled: webView.canGoForward</string>
<reference key="source" ref="36322049"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="36322049"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">enabled: webView.canGoForward</string>
<string key="NSBinding">enabled</string>
<string key="NSKeyPath">webView.canGoForward</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">35</int>
</object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBObjectRecord">
<int key="objectID">0</int>
<object class="NSArray" key="object" id="0">
<bool key="EncodedWithXMLCoder">YES</bool>
</object>
<reference key="children" ref="1000"/>
<nil key="parent"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">-2</int>
<reference key="object" ref="1001"/>
<reference key="parent" ref="0"/>
<string key="objectName">File's Owner</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-1</int>
<reference key="object" ref="1003"/>
<reference key="parent" ref="0"/>
<string key="objectName">First Responder</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-3</int>
<reference key="object" ref="1004"/>
<reference key="parent" ref="0"/>
<string key="objectName">Application</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">3</int>
<reference key="object" ref="996099970"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="563959409"/>
</object>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">4</int>
<reference key="object" ref="563959409"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="697605106"/>
<reference ref="823054555"/>
<reference ref="36322049"/>
<reference ref="771759786"/>
<reference ref="736908656"/>
</object>
<reference key="parent" ref="996099970"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">5</int>
<reference key="object" ref="697605106"/>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">17</int>
<reference key="object" ref="736908656"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="215388286"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">18</int>
<reference key="object" ref="215388286"/>
<reference key="parent" ref="736908656"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">19</int>
<reference key="object" ref="771759786"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="656055052"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">20</int>
<reference key="object" ref="656055052"/>
<reference key="parent" ref="771759786"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">26</int>
<reference key="object" ref="36322049"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="282674264"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">27</int>
<reference key="object" ref="282674264"/>
<reference key="parent" ref="36322049"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">32</int>
<reference key="object" ref="681499023"/>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">43</int>
<reference key="object" ref="823054555"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="817997953"/>
</object>
<reference key="parent" ref="563959409"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">44</int>
<reference key="object" ref="817997953"/>
<reference key="parent" ref="823054555"/>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="flattenedProperties">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>-1.IBPluginDependency</string>
<string>-2.IBPluginDependency</string>
<string>-3.IBPluginDependency</string>
<string>17.IBPluginDependency</string>
<string>18.IBPluginDependency</string>
<string>19.IBPluginDependency</string>
<string>20.IBPluginDependency</string>
<string>26.IBPluginDependency</string>
<string>27.IBPluginDependency</string>
<string>3.IBPluginDependency</string>
<string>3.IBWindowTemplateEditedContentRect</string>
<string>3.NSWindowTemplate.visibleAtLaunch</string>
<string>32.IBPluginDependency</string>
<string>4.IBPluginDependency</string>
<string>43.IBPluginDependency</string>
<string>44.IBPluginDependency</string>
<string>5.IBPluginDependency</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>{{112, 709}, {515, 419}}</string>
<boolean value="NO"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
</object>
<object class="NSMutableDictionary" key="unlocalizedProperties">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference key="dict.sortedKeys" ref="0"/>
<reference key="dict.values" ref="0"/>
</object>
<nil key="activeLocalization"/>
<object class="NSMutableDictionary" key="localizations">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference key="dict.sortedKeys" ref="0"/>
<reference key="dict.values" ref="0"/>
</object>
<nil key="sourceID"/>
<int key="maxID">50</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBPartialClassDescription">
<string key="className">GTMOAuth2WindowController</string>
<string key="superclassName">NSWindowController</string>
<object class="NSMutableDictionary" key="actions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>closeWindow:</string>
<string>toggleStorePasswordInKeychain:</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>id</string>
<string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>closeWindow:</string>
<string>toggleStorePasswordInKeychain:</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">closeWindow:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">toggleStorePasswordInKeychain:</string>
<string key="candidateClassName">id</string>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>keychainCheckbox</string>
<string>webBackButton</string>
<string>webCloseButton</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSButton</string>
<string>NSButton</string>
<string>NSButton</string>
<string>WebView</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>keychainCheckbox</string>
<string>webBackButton</string>
<string>webCloseButton</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">keychainCheckbox</string>
<string key="candidateClassName">NSButton</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">webBackButton</string>
<string key="candidateClassName">NSButton</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">webCloseButton</string>
<string key="candidateClassName">NSButton</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">webView</string>
<string key="candidateClassName">WebView</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/GTMOAuth2WindowController.h</string>
</object>
</object>
</object>
</object>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>
</object>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string>
<integer value="3000" key="NS.object.0"/>
</object>
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSGoLeftTemplate</string>
<string>NSGoRightTemplate</string>
<string>NSStopProgressTemplate</string>
<string>NSSwitch</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>{9, 9}</string>
<string>{9, 9}</string>
<string>{11, 11}</string>
<string>{15, 15}</string>
</object>
</object>
</data>
</archive>