diff options
24 files changed, 10693 insertions, 4058 deletions
diff --git a/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.h b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.h new file mode 100755 index 00000000..96018f5d --- /dev/null +++ b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.h @@ -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 diff --git a/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.m b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.m new file mode 100755 index 00000000..2c859230 --- /dev/null +++ b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.m @@ -0,0 +1,605 @@ +/* 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 = (NSInteger)[cookies_ count] - 1; idx >= 0; idx--) { + + NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:(NSUInteger)idx]; + + NSDate *expiresDate = [storedCookie expiresDate]; + if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) { + [cookies_ removeObjectAtIndex:(NSUInteger)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 { + @synchronized(self) { + 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 { + @synchronized(self) { + 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 { + // Internal routine. + 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 { + @synchronized(self) { + 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 { + @synchronized(self) { + [etaggedDataCache_ removeCachedResponseForRequest:request]; + } +} + +- (void)clearETaggedDataCache { + @synchronized(self) { + [etaggedDataCache_ removeAllCachedResponses]; + } +} + +- (void)clearHistory { + @synchronized(self) { + [self clearETaggedDataCache]; + [cookieStorage_ removeAllCookies]; + } +} + +- (void)removeAllCookies { + @synchronized(self) { + [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 diff --git a/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.h b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.h new file mode 100755 index 00000000..cd9a9ff6 --- /dev/null +++ b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.h @@ -0,0 +1,766 @@ +/* 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> +// +// +// Threading and queue support: +// +// Callbacks require either that the thread used to start the fetcher have a run +// loop spinning (typically the main thread), or that an NSOperationQueue be +// provided upon which the delegate callbacks will be called. Starting with +// iOS 6 and Mac OS X 10.7, clients may simply create an operation queue for +// callbacks on a background thread: +// +// NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease]; +// [queue setMaxConcurrentOperationCount:1]; +// fetcher.delegateQueue = queue; +// +// or specify the main queue for callbacks on the main thread: +// +// fetcher.delegateQueue = [NSOperationQueue mainQueue]; +// +// The client may also re-dispatch from the callbacks and notifications to +// a known dispatch queue: +// +// [myFetcher beginFetchWithCompletionHandler:^(NSData *retrievedData, NSError *error) { +// if (error == nil) { +// dispatch_async(myDispatchQueue, ^{ +// ... +// }); +// } +// }]; +// +// +// +// 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 + +@property (retain) NSOperationQueue *delegateQueue; + +- (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; + +- (void)stopAuthorizationForRequest:(NSURLRequest *)request; + +- (BOOL)isAuthorizingRequest:(NSURLRequest *)request; + +- (BOOL)isAuthorizedRequest:(NSURLRequest *)request; + +@property (retain, readonly) NSString *userEmail; + +@optional + +// Indicate if authorization may be attempted. Even if this succeeds, +// authorization may fail if the user's permissions have been revoked. +@property (readonly) BOOL canAuthorize; + +// For development only, allow authorization of non-SSL requests, allowing +// transmission of the bearer token unencrypted. +@property (assign) BOOL shouldAuthorizeAllRequests; + +#if NS_BLOCKS_AVAILABLE +- (void)authorizeRequest:(NSMutableURLRequest *)request + completionHandler:(void (^)(NSError *error))handler; +#endif + +@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 + NSOperationQueue *delegateQueue_; // optional; available iOS 6/10.7 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_; +#if !STRIP_GTM_FETCH_LOGGING + NSURL *redirectedFromURL_; + NSString *logRequestBody_; + NSString *logResponseBody_; + BOOL shouldDeferResponseBodyLogging_; +#endif +} + +// 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 when no operation +// queue is provided. +@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:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2); + +// Log of request and response, if logging is enabled +@property (copy) NSString *log; + +// Callbacks can be invoked on an operation queue rather than via the run loop, +// starting on 10.7 and iOS 6. If a delegate queue is supplied. the run loop +// modes are ignored. +@property (retain) NSOperationQueue *delegateQueue; + +// 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 diff --git a/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m new file mode 100755 index 00000000..ebbdd0de --- /dev/null +++ b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m @@ -0,0 +1,1956 @@ +/* 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.m +// + +#define GTMHTTPFETCHER_DEFINE_GLOBALS 1 + +#import "GTMHTTPFetcher.h" + +#if GTM_BACKGROUND_FETCHING +#import <UIKit/UIKit.h> +#endif + +static id <GTMCookieStorageProtocol> gGTMFetcherStaticCookieStorage = nil; +static Class gGTMFetcherConnectionClass = nil; + +// The default max retry interview is 10 minutes for uploads (POST/PUT/PATCH), +// 1 minute for downloads. +static const NSTimeInterval kUnsetMaxRetryInterval = -1; +static const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0; +static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.; + +// delegateQueue callback parameters +static NSString *const kCallbackData = @"data"; +static NSString *const kCallbackError = @"error"; + +// +// GTMHTTPFetcher +// + +@interface GTMHTTPFetcher () + +@property (copy) NSString *temporaryDownloadPath; +@property (retain) id <GTMCookieStorageProtocol> cookieStorage; +@property (readwrite, retain) NSData *downloadedData; +#if NS_BLOCKS_AVAILABLE +@property (copy) void (^completionBlock)(NSData *, NSError *); +#endif + +- (BOOL)beginFetchMayDelay:(BOOL)mayDelay + mayAuthorize:(BOOL)mayAuthorize; +- (void)failToBeginFetchWithError:(NSError *)error; +- (void)failToBeginFetchDeferWithError:(NSError *)error; + +#if GTM_BACKGROUND_FETCHING +- (void)endBackgroundTask; +- (void)backgroundFetchExpired; +#endif + +- (BOOL)authorizeRequest; +- (void)authorizer:(id <GTMFetcherAuthorizationProtocol>)auth + request:(NSMutableURLRequest *)request + finishedWithError:(NSError *)error; + +- (NSString *)createTempDownloadFilePathForPath:(NSString *)targetPath; +- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks; +- (BOOL)shouldReleaseCallbacksUponCompletion; + +- (void)addCookiesToRequest:(NSMutableURLRequest *)request; +- (void)handleCookiesForResponse:(NSURLResponse *)response; + +- (void)invokeFetchCallbacksWithData:(NSData *)data + error:(NSError *)error; +- (void)invokeFetchCallback:(SEL)sel + target:(id)target + data:(NSData *)data + error:(NSError *)error; +- (void)invokeFetchCallbacksOnDelegateQueueWithData:(NSData *)data + error:(NSError *)error; +- (void)releaseCallbacks; + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error; + +- (BOOL)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error; +- (void)destroyRetryTimer; +- (void)beginRetryTimer; +- (void)primeRetryTimerWithNewTimeInterval:(NSTimeInterval)secs; +- (void)sendStopNotificationIfNeeded; +- (void)retryFetch; +- (void)retryTimerFired:(NSTimer *)timer; +@end + +@interface GTMHTTPFetcher (GTMHTTPFetcherLoggingInternal) +- (void)setupStreamLogging; +- (void)logFetchWithError:(NSError *)error; +@end + +@implementation GTMHTTPFetcher + ++ (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request { + return [[[[self class] alloc] initWithRequest:request] autorelease]; +} + ++ (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL { + return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]]; +} + ++ (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString { + return [self fetcherWithURL:[NSURL URLWithString:requestURLString]]; +} + ++ (void)initialize { + // initialize is guaranteed by the runtime to be called in a + // thread-safe manner + if (!gGTMFetcherStaticCookieStorage) { + Class cookieStorageClass = NSClassFromString(@"GTMCookieStorage"); + if (cookieStorageClass) { + gGTMFetcherStaticCookieStorage = [[cookieStorageClass alloc] init]; + } + } +} + +- (id)init { + return [self initWithRequest:nil]; +} + +- (id)initWithRequest:(NSURLRequest *)request { + self = [super init]; + if (self) { + request_ = [request mutableCopy]; + + if (gGTMFetcherStaticCookieStorage != nil) { + // The user has compiled with the cookie storage class available; + // default to static cookie storage, so our cookies are independent + // of the cookies of other apps. + [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic]; + } else { + // Default to system default cookie storage + [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodSystemDefault]; + } +#if !STRIP_GTM_FETCH_LOGGING + // Encourage developers to set the comment property or use + // setCommentWithFormat: by providing a default string. + comment_ = @"(No fetcher comment set)"; +#endif + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + // disallow use of fetchers in a copy property + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p (%@)", + [self class], self, [self.mutableRequest URL]]; +} + +#if !GTM_IPHONE +- (void)finalize { + [self stopFetchReleasingCallbacks:YES]; // releases connection_, destroys timers + [super finalize]; +} +#endif + +- (void)dealloc { +#if DEBUG + NSAssert(!isStopNotificationNeeded_, + @"unbalanced fetcher notification for %@", [request_ URL]); +#endif + + // Note: if a connection or a retry timer was pending, then this instance + // would be retained by those so it wouldn't be getting dealloc'd, + // hence we don't need to stopFetch here + [request_ release]; + [connection_ release]; + [downloadedData_ release]; + [downloadPath_ release]; + [temporaryDownloadPath_ release]; + [downloadFileHandle_ release]; + [credential_ release]; + [proxyCredential_ release]; + [postData_ release]; + [postStream_ release]; + [loggedStreamData_ release]; + [response_ release]; +#if NS_BLOCKS_AVAILABLE + [completionBlock_ release]; + [receivedDataBlock_ release]; + [sentDataBlock_ release]; + [retryBlock_ release]; +#endif + [userData_ release]; + [properties_ release]; + [delegateQueue_ release]; + [runLoopModes_ release]; + [fetchHistory_ release]; + [cookieStorage_ release]; + [authorizer_ release]; + [service_ release]; + [serviceHost_ release]; + [thread_ release]; + [retryTimer_ release]; + [comment_ release]; + [log_ release]; +#if !STRIP_GTM_FETCH_LOGGING + [redirectedFromURL_ release]; + [logRequestBody_ release]; + [logResponseBody_ release]; +#endif + + [super dealloc]; +} + +#pragma mark - + +// Begin fetching the URL (or begin a retry fetch). The delegate is retained +// for the duration of the fetch connection. + +- (BOOL)beginFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSelector { + GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector, @encode(GTMHTTPFetcher *), @encode(NSData *), @encode(NSError *), 0); + GTMAssertSelectorNilOrImplementedWithArgs(delegate, receivedDataSel_, @encode(GTMHTTPFetcher *), @encode(NSData *), 0); + GTMAssertSelectorNilOrImplementedWithArgs(delegate, retrySel_, @encode(GTMHTTPFetcher *), @encode(BOOL), @encode(NSError *), 0); + + // We'll retain the delegate only during the outstanding connection (similar + // to what Cocoa does with performSelectorOnMainThread:) and during + // authorization or delays, since the app would crash + // if the delegate was released before the fetch calls back + [self setDelegate:delegate]; + finishedSel_ = finishedSelector; + + return [self beginFetchMayDelay:YES + mayAuthorize:YES]; +} + +- (BOOL)beginFetchMayDelay:(BOOL)mayDelay + mayAuthorize:(BOOL)mayAuthorize { + // This is the internal entry point for re-starting fetches + NSError *error = nil; + + if (connection_ != nil) { + NSAssert1(connection_ != nil, @"fetch object %@ being reused; this should never happen", self); + goto CannotBeginFetch; + } + + if (request_ == nil || [request_ URL] == nil) { + NSAssert(request_ != nil, @"beginFetchWithDelegate requires a request with a URL"); + goto CannotBeginFetch; + } + + self.downloadedData = nil; + downloadedLength_ = 0; + + if (mayDelay && service_) { + BOOL shouldFetchNow = [service_ fetcherShouldBeginFetching:self]; + if (!shouldFetchNow) { + // the fetch is deferred, but will happen later + return YES; + } + } + + NSString *effectiveHTTPMethod = [request_ valueForHTTPHeaderField:@"X-HTTP-Method-Override"]; + if (effectiveHTTPMethod == nil) { + effectiveHTTPMethod = [request_ HTTPMethod]; + } + BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil + || [effectiveHTTPMethod isEqual:@"GET"]); + + if (postData_ || postStream_) { + if (isEffectiveHTTPGet) { + [request_ setHTTPMethod:@"POST"]; + isEffectiveHTTPGet = NO; + } + + if (postData_) { + [request_ setHTTPBody:postData_]; + } else { + if ([self respondsToSelector:@selector(setupStreamLogging)]) { + [self performSelector:@selector(setupStreamLogging)]; + } + + [request_ setHTTPBodyStream:postStream_]; + } + } + + // We authorize after setting up the http method and body in the request + // because OAuth 1 may need to sign the request body + if (mayAuthorize && authorizer_) { + BOOL isAuthorized = [authorizer_ isAuthorizedRequest:request_]; + if (!isAuthorized) { + // authorization needed + return [self authorizeRequest]; + } + } + + [fetchHistory_ updateRequest:request_ isHTTPGet:isEffectiveHTTPGet]; + + // set the default upload or download retry interval, if necessary + if (isRetryEnabled_ + && maxRetryInterval_ <= kUnsetMaxRetryInterval) { + if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) { + [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval]; + } else { + [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval]; + } + } + + [self addCookiesToRequest:request_]; + + if (downloadPath_ != nil) { + // downloading to a path, so create a temporary file and a file handle for + // downloading + NSString *tempPath = [self createTempDownloadFilePathForPath:downloadPath_]; + + BOOL didCreate = [[NSData data] writeToFile:tempPath + options:0 + error:&error]; + if (!didCreate) goto CannotBeginFetch; + + [self setTemporaryDownloadPath:tempPath]; + + NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:tempPath]; + if (fh == nil) goto CannotBeginFetch; + + [self setDownloadFileHandle:fh]; + } + + // finally, start the connection + + Class connectionClass = [[self class] connectionClass]; + + NSOperationQueue *delegateQueue = delegateQueue_; + if (delegateQueue && + ![connectionClass instancesRespondToSelector:@selector(setDelegateQueue:)]) { + // NSURLConnection has no setDelegateQueue: on iOS 4 and Mac OS X 10.5. + delegateQueue = nil; + self.delegateQueue = nil; + } + +#if DEBUG && TARGET_OS_IPHONE + BOOL isPreIOS6 = (NSFoundationVersionNumber <= 890.1); + if (isPreIOS6 && delegateQueue) { + NSLog(@"GTMHTTPFetcher delegateQueue not safe in iOS 5"); + } +#endif + + if (downloadFileHandle_ != nil) { + // Downloading to a file, so downloadedData_ remains nil. + } else { + self.downloadedData = [NSMutableData data]; + } + + hasConnectionEnded_ = NO; + if ([runLoopModes_ count] == 0 && delegateQueue == nil) { + // No custom callback modes or queue were specified, so start the connection + // on the current run loop in the current mode + connection_ = [[connectionClass connectionWithRequest:request_ + delegate:self] retain]; + } else { + // Specify callbacks be on an operation queue or on the current run loop + // in the specified modes + connection_ = [[connectionClass alloc] initWithRequest:request_ + delegate:self + startImmediately:NO]; + if (delegateQueue) { + [connection_ performSelector:@selector(setDelegateQueue:) + withObject:delegateQueue]; + } else if (runLoopModes_) { + NSRunLoop *rl = [NSRunLoop currentRunLoop]; + for (NSString *mode in runLoopModes_) { + [connection_ scheduleInRunLoop:rl forMode:mode]; + } + } + [connection_ start]; + } + + if (!connection_) { + NSAssert(connection_ != nil, @"beginFetchWithDelegate could not create a connection"); + self.downloadedData = nil; + goto CannotBeginFetch; + } + +#if GTM_BACKGROUND_FETCHING + backgroundTaskIdentifer_ = 0; // UIBackgroundTaskInvalid is 0 on iOS 4 + if (shouldFetchInBackground_) { + // For iOS 3 compatibility, ensure that UIApp supports backgrounding + UIApplication *app = [UIApplication sharedApplication]; + if ([app respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)]) { + // Tell UIApplication that we want to continue even when the app is in the + // background. + NSThread *thread = delegateQueue_ ? nil : [NSThread currentThread]; + backgroundTaskIdentifer_ = [app beginBackgroundTaskWithExpirationHandler:^{ + // Background task expiration callback - this block is always invoked by + // UIApplication on the main thread. + if (thread) { + // Run the user's callbacks on the thread used to start the + // fetch. + [self performSelector:@selector(backgroundFetchExpired) + onThread:thread + withObject:nil + waitUntilDone:YES]; + } else { + // backgroundFetchExpired invokes callbacks on the provided delegate + // queue. + [self backgroundFetchExpired]; + } + }]; + } + } +#endif + + // Once connection_ is non-nil we can send the start notification + isStopNotificationNeeded_ = YES; + NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; + [defaultNC postNotificationName:kGTMHTTPFetcherStartedNotification + object:self]; + return YES; + +CannotBeginFetch: + [self failToBeginFetchDeferWithError:error]; + return NO; +} + +- (void)failToBeginFetchDeferWithError:(NSError *)error { + if (delegateQueue_) { + // Deferring will happen by the callback being invoked on the specified + // queue. + [self failToBeginFetchWithError:error]; + } else { + // No delegate queue has been specified, so put the callback + // on an appropriate run loop. + NSArray *modes = (runLoopModes_ ? runLoopModes_ : + [NSArray arrayWithObject:NSRunLoopCommonModes]); + [self performSelector:@selector(failToBeginFetchWithError:) + onThread:[NSThread currentThread] + withObject:error + waitUntilDone:NO + modes:modes]; + } +} + +- (void)failToBeginFetchWithError:(NSError *)error { + if (error == nil) { + error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain + code:kGTMHTTPFetcherErrorDownloadFailed + userInfo:nil]; + } + + [[self retain] autorelease]; // In case the callback releases us + + [self invokeFetchCallbacksOnDelegateQueueWithData:nil + error:error]; + + [self releaseCallbacks]; + + [service_ fetcherDidStop:self]; + + self.authorizer = nil; + + if (temporaryDownloadPath_) { + [[NSFileManager defaultManager] removeItemAtPath:temporaryDownloadPath_ + error:NULL]; + self.temporaryDownloadPath = nil; + } +} + +#if GTM_BACKGROUND_FETCHING +- (void)backgroundFetchExpired { + // On background expiration, we stop the fetch and invoke the callbacks + NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain + code:kGTMHTTPFetcherErrorBackgroundExpiration + userInfo:nil]; + [self invokeFetchCallbacksOnDelegateQueueWithData:nil + error:error]; + @synchronized(self) { + // Stopping the fetch here will indirectly call endBackgroundTask + [self stopFetchReleasingCallbacks:NO]; + + [self releaseCallbacks]; + self.authorizer = nil; + } +} + +- (void)endBackgroundTask { + @synchronized(self) { + // Whenever the connection stops or background execution expires, + // we need to tell UIApplication we're done + if (backgroundTaskIdentifer_) { + // If backgroundTaskIdentifer_ is non-zero, we know we're on iOS 4 + UIApplication *app = [UIApplication sharedApplication]; + [app endBackgroundTask:backgroundTaskIdentifer_]; + + backgroundTaskIdentifer_ = 0; + } + } +} +#endif // GTM_BACKGROUND_FETCHING + +- (BOOL)authorizeRequest { + id authorizer = self.authorizer; + SEL asyncAuthSel = @selector(authorizeRequest:delegate:didFinishSelector:); + if ([authorizer respondsToSelector:asyncAuthSel]) { + SEL callbackSel = @selector(authorizer:request:finishedWithError:); + [authorizer authorizeRequest:request_ + delegate:self + didFinishSelector:callbackSel]; + return YES; + } else { + NSAssert(authorizer == nil, @"invalid authorizer for fetch"); + + // No authorizing possible, and authorizing happens only after any delay; + // just begin fetching + return [self beginFetchMayDelay:NO + mayAuthorize:NO]; + } +} + +- (void)authorizer:(id <GTMFetcherAuthorizationProtocol>)auth + request:(NSMutableURLRequest *)request + finishedWithError:(NSError *)error { + if (error != nil) { + // We can't fetch without authorization + [self failToBeginFetchDeferWithError:error]; + } else { + [self beginFetchMayDelay:NO + mayAuthorize:NO]; + } +} + +#if NS_BLOCKS_AVAILABLE +- (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler { + self.completionBlock = handler; + + // The user may have called setDelegate: earlier if they want to use other + // delegate-style callbacks during the fetch; otherwise, the delegate is nil, + // which is fine. + return [self beginFetchWithDelegate:[self delegate] + didFinishSelector:nil]; +} +#endif + +- (NSString *)createTempDownloadFilePathForPath:(NSString *)targetPath { + NSString *tempDir = nil; + +#if (!TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED >= 1060)) + // Find an appropriate directory for the download, ideally on the same disk + // as the final target location so the temporary file won't have to be moved + // to a different disk. + // + // Available in SDKs for 10.6 and iOS 4 + // + // Oct 2011: We previously also used URLForDirectory for + // (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 40000)) + // but that is returning a non-temporary directory for iOS, unfortunately + + SEL sel = @selector(URLForDirectory:inDomain:appropriateForURL:create:error:); + if ([NSFileManager instancesRespondToSelector:sel]) { + NSError *error = nil; + NSURL *targetURL = [NSURL fileURLWithPath:targetPath]; + NSFileManager *fileMgr = [NSFileManager defaultManager]; + + NSURL *tempDirURL = [fileMgr URLForDirectory:NSItemReplacementDirectory + inDomain:NSUserDomainMask + appropriateForURL:targetURL + create:YES + error:&error]; + tempDir = [tempDirURL path]; + } +#endif + + if (tempDir == nil) { + tempDir = NSTemporaryDirectory(); + } + + static unsigned int counter = 0; + NSString *name = [NSString stringWithFormat:@"gtmhttpfetcher_%u_%u", + ++counter, (unsigned int) arc4random()]; + NSString *result = [tempDir stringByAppendingPathComponent:name]; + return result; +} + +- (void)addCookiesToRequest:(NSMutableURLRequest *)request { + // Get cookies for this URL from our storage array, if + // we have a storage array + if (cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodSystemDefault + && cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodNone) { + + NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]]; + if ([cookies count] > 0) { + + NSDictionary *headerFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + NSString *cookieHeader = [headerFields objectForKey:@"Cookie"]; // key used in header dictionary + if (cookieHeader) { + [request addValue:cookieHeader forHTTPHeaderField:@"Cookie"]; // header name + } + } + } +} + +// Returns YES if this is in the process of fetching a URL, or waiting to +// retry, or waiting for authorization, or waiting to be issued by the +// service object +- (BOOL)isFetching { + if (connection_ != nil || retryTimer_ != nil) return YES; + + BOOL isAuthorizing = [authorizer_ isAuthorizingRequest:request_]; + if (isAuthorizing) return YES; + + BOOL isDelayed = [service_ isDelayingFetcher:self]; + return isDelayed; +} + +// Returns the status code set in connection:didReceiveResponse: +- (NSInteger)statusCode { + + NSInteger statusCode; + + if (response_ != nil + && [response_ respondsToSelector:@selector(statusCode)]) { + + statusCode = [(NSHTTPURLResponse *)response_ statusCode]; + } else { + // Default to zero, in hopes of hinting "Unknown" (we can't be + // sure that things are OK enough to use 200). + statusCode = 0; + } + return statusCode; +} + +- (NSDictionary *)responseHeaders { + if (response_ != nil + && [response_ respondsToSelector:@selector(allHeaderFields)]) { + + NSDictionary *headers = [(NSHTTPURLResponse *)response_ allHeaderFields]; + return headers; + } + return nil; +} + +- (void)releaseCallbacks { + [delegate_ autorelease]; + delegate_ = nil; + + [delegateQueue_ autorelease]; + delegateQueue_ = nil; + +#if NS_BLOCKS_AVAILABLE + self.completionBlock = nil; + self.sentDataBlock = nil; + self.receivedDataBlock = nil; + self.retryBlock = nil; +#endif +} + +// Cancel the fetch of the URL that's currently in progress. +- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks { + id <GTMHTTPFetcherServiceProtocol> service; + + // if the connection or the retry timer is all that's retaining the fetcher, + // we want to be sure this instance survives stopping at least long enough for + // the stack to unwind + [[self retain] autorelease]; + + [self destroyRetryTimer]; + + @synchronized(self) { + service = [[service_ retain] autorelease]; + + if (connection_) { + // in case cancelling the connection calls this recursively, we want + // to ensure that we'll only release the connection and delegate once, + // so first set connection_ to nil + NSURLConnection* oldConnection = connection_; + connection_ = nil; + + if (!hasConnectionEnded_) { + [oldConnection cancel]; + } + + // this may be called in a callback from the connection, so use autorelease + [oldConnection autorelease]; + } + } // @synchronized(self) + + // send the stopped notification + [self sendStopNotificationIfNeeded]; + + @synchronized(self) { + [authorizer_ stopAuthorizationForRequest:request_]; + + if (shouldReleaseCallbacks) { + [self releaseCallbacks]; + + self.authorizer = nil; + } + + if (temporaryDownloadPath_) { + [[NSFileManager defaultManager] removeItemAtPath:temporaryDownloadPath_ + error:NULL]; + self.temporaryDownloadPath = nil; + } + } // @synchronized(self) + + [service fetcherDidStop:self]; + +#if GTM_BACKGROUND_FETCHING + [self endBackgroundTask]; +#endif +} + +// External stop method +- (void)stopFetching { + [self stopFetchReleasingCallbacks:YES]; +} + +- (void)sendStopNotificationIfNeeded { + BOOL sendNow = NO; + @synchronized(self) { + if (isStopNotificationNeeded_) { + isStopNotificationNeeded_ = NO; + sendNow = YES; + } + } + + if (sendNow) { + NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; + [defaultNC postNotificationName:kGTMHTTPFetcherStoppedNotification + object:self]; + } +} + +- (void)retryFetch { + [self stopFetchReleasingCallbacks:NO]; + + [self beginFetchWithDelegate:delegate_ + didFinishSelector:finishedSel_]; +} + +- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds { + NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds]; + + // Loop until the callbacks have been called and released, and until + // the connection is no longer pending, or until the timeout has expired + BOOL isMainThread = [NSThread isMainThread]; + + while ((!hasConnectionEnded_ +#if NS_BLOCKS_AVAILABLE + || completionBlock_ != nil +#endif + || delegate_ != nil) + && [giveUpDate timeIntervalSinceNow] > 0) { + + // Run the current run loop 1/1000 of a second to give the networking + // code a chance to work + if (isMainThread || delegateQueue_ == nil) { + NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001]; + [[NSRunLoop currentRunLoop] runUntilDate:stopDate]; + } else { + [NSThread sleepForTimeInterval:0.001]; + } + } +} + +#pragma mark NSURLConnection Delegate Methods + +// +// NSURLConnection Delegate Methods +// + +// This method just says "follow all redirects", which _should_ be the default behavior, +// According to file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/Conceptual/URLLoadingSystem +// but the redirects were not being followed until I added this method. May be +// a bug in the NSURLConnection code, or the documentation. +// +// In OS X 10.4.8 and earlier, the redirect request doesn't +// get the original's headers and body. This causes POSTs to fail. +// So we construct a new request, a copy of the original, with overrides from the +// redirect. +// +// Docs say that if redirectResponse is nil, just return the redirectRequest. + +- (NSURLRequest *)connection:(NSURLConnection *)connection + willSendRequest:(NSURLRequest *)redirectRequest + redirectResponse:(NSURLResponse *)redirectResponse { + @synchronized(self) { + if (redirectRequest && redirectResponse) { + // save cookies from the response + [self handleCookiesForResponse:redirectResponse]; + + NSMutableURLRequest *newRequest = [[request_ mutableCopy] autorelease]; + // copy the URL + NSURL *redirectURL = [redirectRequest URL]; + NSURL *url = [newRequest URL]; + + // disallow scheme changes (say, from https to http) + NSString *redirectScheme = [url scheme]; + NSString *newScheme = [redirectURL scheme]; + NSString *newResourceSpecifier = [redirectURL resourceSpecifier]; + + if ([redirectScheme caseInsensitiveCompare:@"http"] == NSOrderedSame + && newScheme != nil + && [newScheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { + + // allow the change from http to https + redirectScheme = newScheme; + } + + NSString *newUrlString = [NSString stringWithFormat:@"%@:%@", + redirectScheme, newResourceSpecifier]; + + NSURL *newURL = [NSURL URLWithString:newUrlString]; + [newRequest setURL:newURL]; + + // any headers in the redirect override headers in the original. + NSDictionary *redirectHeaders = [redirectRequest allHTTPHeaderFields]; + for (NSString *key in redirectHeaders) { + NSString *value = [redirectHeaders objectForKey:key]; + [newRequest setValue:value forHTTPHeaderField:key]; + } + + [self addCookiesToRequest:newRequest]; + + redirectRequest = newRequest; + + // log the response we just received + [self setResponse:redirectResponse]; + [self logNowWithError:nil]; + + // update the request for future logging + NSMutableURLRequest *mutable = [[redirectRequest mutableCopy] autorelease]; + [self setMutableRequest:mutable]; + } + return redirectRequest; + } // @synchronized(self) +} + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { + @synchronized(self) { + // This method is called when the server has determined that it + // has enough information to create the NSURLResponse + // it can be called multiple times, for example in the case of a + // redirect, so each time we reset the data. + [downloadedData_ setLength:0]; + [downloadFileHandle_ truncateFileAtOffset:0]; + downloadedLength_ = 0; + + [self setResponse:response]; + + // Save cookies from the response + [self handleCookiesForResponse:response]; + } +} + + +// handleCookiesForResponse: handles storage of cookies for responses passed to +// connection:willSendRequest:redirectResponse: and connection:didReceiveResponse: +- (void)handleCookiesForResponse:(NSURLResponse *)response { + + if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodSystemDefault + || cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodNone) { + + // do nothing special for NSURLConnection's default storage mechanism + // or when we're ignoring cookies + + } else if ([response respondsToSelector:@selector(allHeaderFields)]) { + + // grab the cookies from the header as NSHTTPCookies and store them either + // into our static array or into the fetchHistory + + NSDictionary *responseHeaderFields = [(NSHTTPURLResponse *)response allHeaderFields]; + if (responseHeaderFields) { + + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseHeaderFields + forURL:[response URL]]; + if ([cookies count] > 0) { + [cookieStorage_ setCookies:cookies]; + } + } + } +} + +-(void)connection:(NSURLConnection *)connection +didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { + @synchronized(self) { + if ([challenge previousFailureCount] <= 2) { + + NSURLCredential *credential = credential_; + + if ([[challenge protectionSpace] isProxy] && proxyCredential_ != nil) { + credential = proxyCredential_; + } + + // Here, if credential is still nil, then we *could* try to get it from + // NSURLCredentialStorage's defaultCredentialForProtectionSpace:. + // We don't, because we're assuming: + // + // - for server credentials, we only want ones supplied by the program + // calling http fetcher + // - for proxy credentials, if one were necessary and available in the + // keychain, it would've been found automatically by NSURLConnection + // and this challenge delegate method never would've been called + // anyway + + if (credential) { + // try the credential + [[challenge sender] useCredential:credential + forAuthenticationChallenge:challenge]; + return; + } + } // @synchronized(self) + + // If we don't have credentials, or we've already failed auth 3x, + // report the error, putting the challenge as a value in the userInfo + // dictionary. +#if DEBUG + NSAssert(!isCancellingChallenge_, @"isCancellingChallenge_ unexpected"); +#endif + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:challenge + forKey:kGTMHTTPFetcherErrorChallengeKey]; + NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain + code:kGTMHTTPFetcherErrorAuthenticationChallengeFailed + userInfo:userInfo]; + + // cancelAuthenticationChallenge seems to indirectly call + // connection:didFailWithError: now, though that isn't documented + // + // We'll use an ivar to make the indirect invocation of the + // delegate method do nothing. + isCancellingChallenge_ = YES; + [[challenge sender] cancelAuthenticationChallenge:challenge]; + isCancellingChallenge_ = NO; + + [self connection:connection didFailWithError:error]; + } +} + +- (void)invokeFetchCallbacksWithData:(NSData *)data + error:(NSError *)error { + // To avoid deadlocks, this should not be called inside of @synchronized(self) + id target; + SEL sel; +#if NS_BLOCKS_AVAILABLE + void (^block)(NSData *, NSError *); +#endif + @synchronized(self) { + target = delegate_; + sel = finishedSel_; + block = completionBlock_; + } + + [[self retain] autorelease]; // In case the callback releases us + + [self invokeFetchCallback:sel + target:target + data:data + error:error]; + +#if NS_BLOCKS_AVAILABLE + if (block) { + block(data, error); + } +#endif +} + +- (void)invokeFetchCallback:(SEL)sel + target:(id)target + data:(NSData *)data + error:(NSError *)error { + // This method is available to subclasses which may provide a customized + // target pointer. + if (target && sel) { + NSMethodSignature *sig = [target methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:target]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&data atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + } +} + +- (void)invokeFetchCallbacksOnDelegateQueueWithData:(NSData *)data + error:(NSError *)error { + // This is called by methods that are not already on the delegateQueue + // (as NSURLConnection callbacks should already be, but other failures + // are not.) + if (!delegateQueue_) { + [self invokeFetchCallbacksWithData:data error:error]; + } + + // Values may be nil. + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:2]; + [dict setValue:data forKey:kCallbackData]; + [dict setValue:error forKey:kCallbackError]; + NSInvocationOperation *op = + [[[NSInvocationOperation alloc] initWithTarget:self + selector:@selector(invokeOnQueueWithDictionary:) + object:dict] autorelease]; + [delegateQueue_ addOperation:op]; +} + +- (void)invokeOnQueueWithDictionary:(NSDictionary *)dict { + NSData *data = [dict objectForKey:kCallbackData]; + NSError *error = [dict objectForKey:kCallbackError]; + + [self invokeFetchCallbacksWithData:data error:error]; +} + + +- (void)invokeSentDataCallback:(SEL)sel + target:(id)target + didSendBodyData:(NSInteger)bytesWritten + totalBytesWritten:(NSInteger)totalBytesWritten + totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { + if (target && sel) { + NSMethodSignature *sig = [target methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:target]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&bytesWritten atIndex:3]; + [invocation setArgument:&totalBytesWritten atIndex:4]; + [invocation setArgument:&totalBytesExpectedToWrite atIndex:5]; + [invocation invoke]; + } +} + +- (BOOL)invokeRetryCallback:(SEL)sel + target:(id)target + willRetry:(BOOL)willRetry + error:(NSError *)error { + if (target && sel) { + NSMethodSignature *sig = [target methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:target]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&willRetry atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + + [invocation getReturnValue:&willRetry]; + } + return willRetry; +} + +- (void)connection:(NSURLConnection *)connection + didSendBodyData:(NSInteger)bytesWritten + totalBytesWritten:(NSInteger)totalBytesWritten +totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { + @synchronized(self) { + SEL sel = [self sentDataSelector]; + [self invokeSentDataCallback:sel + target:delegate_ + didSendBodyData:bytesWritten + totalBytesWritten:totalBytesWritten + totalBytesExpectedToWrite:totalBytesExpectedToWrite]; + +#if NS_BLOCKS_AVAILABLE + if (sentDataBlock_) { + sentDataBlock_(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite); + } +#endif + } +} + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { + @synchronized(self) { +#if DEBUG + NSAssert(!hasConnectionEnded_, @"Connection received data after ending"); + + // The download file handle should be set or the data object allocated + // before the fetch is started. + NSAssert((downloadFileHandle_ == nil) != (downloadedData_ == nil), + @"received data accumulates as either NSData (%d) or" + @" NSFileHandle (%d)", + (downloadedData_ != nil), (downloadFileHandle_ != nil)); +#endif + // Hopefully, we'll never see this execute out-of-order, receiving data + // after we've received the finished or failed callback. + if (hasConnectionEnded_) return; + + if (downloadFileHandle_ != nil) { + // Append to file + @try { + [downloadFileHandle_ writeData:data]; + + downloadedLength_ = [downloadFileHandle_ offsetInFile]; + } + @catch (NSException *exc) { + // Couldn't write to file, probably due to a full disk + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[exc reason] + forKey:NSLocalizedDescriptionKey]; + NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain + code:kGTMHTTPFetcherErrorFileHandleException + userInfo:userInfo]; + [self connection:connection didFailWithError:error]; + return; + } + } else { + // append to mutable data + [downloadedData_ appendData:data]; + + downloadedLength_ = [downloadedData_ length]; + } + + if (receivedDataSel_) { + [delegate_ performSelector:receivedDataSel_ + withObject:self + withObject:downloadedData_]; + } + +#if NS_BLOCKS_AVAILABLE + if (receivedDataBlock_) { + receivedDataBlock_(downloadedData_); + } +#endif + } // @synchronized(self) +} + +// For error 304's ("Not Modified") where we've cached the data, return +// status 200 ("OK") to the caller (but leave the fetcher status as 304) +// and copy the cached data. +// +// For other errors or if there's no cached data, just return the actual status. +- (NSData *)cachedDataForStatus { + if ([self statusCode] == kGTMHTTPFetcherStatusNotModified + && [fetchHistory_ shouldCacheETaggedData]) { + NSData *cachedData = [fetchHistory_ cachedDataForRequest:request_]; + return cachedData; + } + return nil; +} + +- (NSInteger)statusAfterHandlingNotModifiedError { + NSInteger status = [self statusCode]; + NSData *cachedData = [self cachedDataForStatus]; + if (cachedData) { + // Forge the status to pass on to the delegate + status = 200; + + // Copy our stored data + if (downloadFileHandle_ != nil) { + @try { + // Downloading to a file handle won't save to the cache (the data is + // likely inappropriately large for caching), but will still read from + // the cache, on the unlikely chance that the response was Not Modified + // and the URL response was indeed present in the cache. + [downloadFileHandle_ truncateFileAtOffset:0]; + [downloadFileHandle_ writeData:cachedData]; + downloadedLength_ = [downloadFileHandle_ offsetInFile]; + } + @catch (NSException *) { + // Failed to write data, likely due to lack of disk space + status = kGTMHTTPFetcherErrorFileHandleException; + } + } else { + [downloadedData_ setData:cachedData]; + downloadedLength_ = [cachedData length]; + } + } + return status; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection { + BOOL shouldStopFetching = YES; + BOOL shouldSendStopNotification = NO; + NSError *error = nil; + NSData *downloadedData; +#if !STRIP_GTM_FETCH_LOGGING + BOOL shouldDeferLogging = NO; +#endif + BOOL shouldBeginRetryTimer = NO; + BOOL hasLogged = NO; + + @synchronized(self) { + // We no longer need to cancel the connection + hasConnectionEnded_ = YES; + + // Skip caching ETagged results when the data is being saved to a file + if (downloadFileHandle_ == nil) { + [fetchHistory_ updateFetchHistoryWithRequest:request_ + response:response_ + downloadedData:downloadedData_]; + } else { + [fetchHistory_ removeCachedDataForRequest:request_]; + } + + [[self retain] autorelease]; // in case the callback releases us + + NSInteger status = [self statusCode]; + if ([self cachedDataForStatus] != nil) { + // Log the pre-cache response. + [self logNowWithError:nil]; + hasLogged = YES; + status = [self statusAfterHandlingNotModifiedError]; + } + + shouldSendStopNotification = YES; + + if (status >= 0 && status < 300) { + // success + if (downloadPath_) { + // Avoid deleting the downloaded file when the fetch stops + [downloadFileHandle_ closeFile]; + self.downloadFileHandle = nil; + + NSFileManager *fileMgr = [NSFileManager defaultManager]; + [fileMgr removeItemAtPath:downloadPath_ + error:NULL]; + + if ([fileMgr moveItemAtPath:temporaryDownloadPath_ + toPath:downloadPath_ + error:&error]) { + self.temporaryDownloadPath = nil; + } + } + } else { + // unsuccessful + if (!hasLogged) { + [self logNowWithError:nil]; + hasLogged = YES; + } + // Status over 300; retry or notify the delegate of failure + if ([self shouldRetryNowForStatus:status error:nil]) { + // retrying + shouldBeginRetryTimer = YES; + shouldStopFetching = NO; + } else { + NSDictionary *userInfo = nil; + if ([downloadedData_ length] > 0) { + userInfo = [NSDictionary dictionaryWithObject:downloadedData_ + forKey:kGTMHTTPFetcherStatusDataKey]; + } + error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain + code:status + userInfo:userInfo]; + } + } + downloadedData = downloadedData_; +#if !STRIP_GTM_FETCH_LOGGING + shouldDeferLogging = shouldDeferResponseBodyLogging_; +#endif + } // @synchronized(self) + + if (shouldBeginRetryTimer) { + [self beginRetryTimer]; + } + + if (shouldSendStopNotification) { + // We want to send the stop notification before calling the delegate's + // callback selector, since the callback selector may release all of + // the fetcher properties that the client is using to track the fetches. + // + // We'll also stop now so that, to any observers watching the notifications, + // it doesn't look like our wait for a retry (which may be long, + // 30 seconds or more) is part of the network activity. + [self sendStopNotificationIfNeeded]; + } + + if (shouldStopFetching) { + // Call the callbacks (outside of the @synchronized to avoid deadlocks.) + [self invokeFetchCallbacksWithData:downloadedData + error:error]; + BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion]; + [self stopFetchReleasingCallbacks:shouldRelease]; + } + + @synchronized(self) { + BOOL shouldLogNow = !hasLogged; +#if !STRIP_GTM_FETCH_LOGGING + if (shouldDeferLogging) shouldLogNow = NO; +#endif + if (shouldLogNow) { + [self logNowWithError:nil]; + } + } +} + +- (BOOL)shouldReleaseCallbacksUponCompletion { + // A subclass can override this to keep callbacks around after the + // connection has finished successfully + return YES; +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { + @synchronized(self) { + // Prevent the failure callback from being called twice, since the stopFetch + // call below (either the explicit one at the end of this method, or the + // implicit one when the retry occurs) will release the delegate. + if (connection_ == nil) return; + + // If this method was invoked indirectly by cancellation of an authentication + // challenge, defer this until it is called again with the proper error object + if (isCancellingChallenge_) return; + + // We no longer need to cancel the connection + hasConnectionEnded_ = YES; + + [self logNowWithError:error]; + } + + // See comment about sendStopNotificationIfNeeded + // in connectionDidFinishLoading: + [self sendStopNotificationIfNeeded]; + + if ([self shouldRetryNowForStatus:0 error:error]) { + [self beginRetryTimer]; + } else { + [[self retain] autorelease]; // in case the callback releases us + + [self invokeFetchCallbacksWithData:nil + error:error]; + + [self stopFetchReleasingCallbacks:YES]; + } +} + +- (void)logNowWithError:(NSError *)error { + // If the logging category is available, then log the current request, + // response, data, and error + if ([self respondsToSelector:@selector(logFetchWithError:)]) { + [self performSelector:@selector(logFetchWithError:) withObject:error]; + } +} + +#pragma mark Retries + +- (BOOL)isRetryError:(NSError *)error { + + struct retryRecord { + NSString *const domain; + int code; + }; + + struct retryRecord retries[] = { + { kGTMHTTPFetcherStatusDomain, 408 }, // request timeout + { kGTMHTTPFetcherStatusDomain, 503 }, // service unavailable + { kGTMHTTPFetcherStatusDomain, 504 }, // request timeout + { NSURLErrorDomain, NSURLErrorTimedOut }, + { NSURLErrorDomain, NSURLErrorNetworkConnectionLost }, + { nil, 0 } + }; + + // NSError's isEqual always returns false for equal but distinct instances + // of NSError, so we have to compare the domain and code values explicitly + + for (int idx = 0; retries[idx].domain != nil; idx++) { + + if ([[error domain] isEqual:retries[idx].domain] + && [error code] == retries[idx].code) { + + return YES; + } + } + return NO; +} + + +// shouldRetryNowForStatus:error: returns YES if the user has enabled retries +// and the status or error is one that is suitable for retrying. "Suitable" +// means either the isRetryError:'s list contains the status or error, or the +// user's retrySelector: is present and returns YES when called, or the +// authorizer may be able to fix. +- (BOOL)shouldRetryNowForStatus:(NSInteger)status + error:(NSError *)error { + // Determine if a refreshed authorizer may avoid an authorization error + BOOL shouldRetryForAuthRefresh = NO; + BOOL isFirstAuthError = (authorizer_ != nil) + && !hasAttemptedAuthRefresh_ + && (status == kGTMHTTPFetcherStatusUnauthorized); // 401 + + if (isFirstAuthError) { + if ([authorizer_ respondsToSelector:@selector(primeForRefresh)]) { + BOOL hasPrimed = [authorizer_ primeForRefresh]; + if (hasPrimed) { + shouldRetryForAuthRefresh = YES; + hasAttemptedAuthRefresh_ = YES; + [request_ setValue:nil forHTTPHeaderField:@"Authorization"]; + } + } + } + + // Determine if we're doing exponential backoff retries + BOOL shouldDoIntervalRetry = [self isRetryEnabled] + && ([self nextRetryInterval] < [self maxRetryInterval]); + + BOOL willRetry = NO; + BOOL canRetry = shouldRetryForAuthRefresh || shouldDoIntervalRetry; + if (canRetry) { + // Check if this is a retryable error + if (error == nil) { + // Make an error for the status + NSDictionary *userInfo = nil; + if ([downloadedData_ length] > 0) { + userInfo = [NSDictionary dictionaryWithObject:downloadedData_ + forKey:kGTMHTTPFetcherStatusDataKey]; + } + error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain + code:status + userInfo:userInfo]; + } + + willRetry = shouldRetryForAuthRefresh || [self isRetryError:error]; + + // If the user has installed a retry callback, consult that + willRetry = [self invokeRetryCallback:retrySel_ + target:delegate_ + willRetry:willRetry + error:error]; +#if NS_BLOCKS_AVAILABLE + if (retryBlock_) { + willRetry = retryBlock_(willRetry, error); + } +#endif + } + return willRetry; +} + +- (void)beginRetryTimer { + @synchronized(self) { + if (delegateQueue_ != nil && ![NSThread isMainThread]) { + // A delegate queue is set, so the thread we're running on may not + // have a run loop. We'll defer creating and starting the timer + // until we're on the main thread to ensure it has a run loop. + // (If we weren't supporting 10.5, we could use dispatch_after instead + // of an NSTimer.) + [self performSelectorOnMainThread:_cmd + withObject:nil + waitUntilDone:NO]; + return; + } + } + + NSTimeInterval nextInterval = [self nextRetryInterval]; + NSTimeInterval maxInterval = [self maxRetryInterval]; + NSTimeInterval newInterval = MIN(nextInterval, maxInterval); + + [self primeRetryTimerWithNewTimeInterval:newInterval]; + + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName:kGTMHTTPFetcherRetryDelayStartedNotification + object:self]; +} + +- (void)primeRetryTimerWithNewTimeInterval:(NSTimeInterval)secs { + + [self destroyRetryTimer]; + + @synchronized(self) { + lastRetryInterval_ = secs; + + retryTimer_ = [NSTimer timerWithTimeInterval:secs + target:self + selector:@selector(retryTimerFired:) + userInfo:nil + repeats:NO]; + [retryTimer_ retain]; + + NSRunLoop *timerRL = (self.delegateQueue ? + [NSRunLoop mainRunLoop] : [NSRunLoop currentRunLoop]); + [timerRL addTimer:retryTimer_ + forMode:NSDefaultRunLoopMode]; + } +} + +- (void)retryTimerFired:(NSTimer *)timer { + [self destroyRetryTimer]; + + @synchronized(self) { + retryCount_++; + + [self retryFetch]; + } +} + +- (void)destroyRetryTimer { + BOOL shouldNotify = NO; + @synchronized(self) { + if (retryTimer_) { + [retryTimer_ invalidate]; + [retryTimer_ autorelease]; + retryTimer_ = nil; + shouldNotify = YES; + } + } // @synchronized(self) + + if (shouldNotify) { + NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; + [defaultNC postNotificationName:kGTMHTTPFetcherRetryDelayStoppedNotification + object:self]; + } +} + +- (NSUInteger)retryCount { + return retryCount_; +} + +- (NSTimeInterval)nextRetryInterval { + // The next wait interval is the factor (2.0) times the last interval, + // but never less than the minimum interval. + NSTimeInterval secs = lastRetryInterval_ * retryFactor_; + secs = MIN(secs, maxRetryInterval_); + secs = MAX(secs, minRetryInterval_); + + return secs; +} + +- (BOOL)isRetryEnabled { + return isRetryEnabled_; +} + +- (void)setRetryEnabled:(BOOL)flag { + + if (flag && !isRetryEnabled_) { + // We defer initializing these until the user calls setRetryEnabled + // to avoid using the random number generator if it's not needed. + // However, this means min and max intervals for this fetcher are reset + // as a side effect of calling setRetryEnabled. + // + // Make an initial retry interval random between 1.0 and 2.0 seconds + [self setMinRetryInterval:0.0]; + [self setMaxRetryInterval:kUnsetMaxRetryInterval]; + [self setRetryFactor:2.0]; + lastRetryInterval_ = 0.0; + } + isRetryEnabled_ = flag; +}; + +- (NSTimeInterval)maxRetryInterval { + return maxRetryInterval_; +} + +- (void)setMaxRetryInterval:(NSTimeInterval)secs { + if (secs > 0) { + maxRetryInterval_ = secs; + } else { + maxRetryInterval_ = kUnsetMaxRetryInterval; + } +} + +- (double)minRetryInterval { + return minRetryInterval_; +} + +- (void)setMinRetryInterval:(NSTimeInterval)secs { + if (secs > 0) { + minRetryInterval_ = secs; + } else { + // Set min interval to a random value between 1.0 and 2.0 seconds + // so that if multiple clients start retrying at the same time, they'll + // repeat at different times and avoid overloading the server + minRetryInterval_ = 1.0 + ((double)(arc4random() & 0x0FFFF) / (double) 0x0FFFF); + } +} + +#pragma mark Getters and Setters + +@dynamic cookieStorageMethod, + retryEnabled, + maxRetryInterval, + minRetryInterval, + retryCount, + nextRetryInterval, + statusCode, + responseHeaders, + fetchHistory, + userData, + properties; + +@synthesize mutableRequest = request_, + credential = credential_, + proxyCredential = proxyCredential_, + postData = postData_, + postStream = postStream_, + delegate = delegate_, + authorizer = authorizer_, + service = service_, + serviceHost = serviceHost_, + servicePriority = servicePriority_, + thread = thread_, + sentDataSelector = sentDataSel_, + receivedDataSelector = receivedDataSel_, + retrySelector = retrySel_, + retryFactor = retryFactor_, + response = response_, + downloadedLength = downloadedLength_, + downloadedData = downloadedData_, + downloadPath = downloadPath_, + temporaryDownloadPath = temporaryDownloadPath_, + downloadFileHandle = downloadFileHandle_, + delegateQueue = delegateQueue_, + runLoopModes = runLoopModes_, + comment = comment_, + log = log_, + cookieStorage = cookieStorage_; + +#if NS_BLOCKS_AVAILABLE +@synthesize completionBlock = completionBlock_, + sentDataBlock = sentDataBlock_, + receivedDataBlock = receivedDataBlock_, + retryBlock = retryBlock_; +#endif + +@synthesize shouldFetchInBackground = shouldFetchInBackground_; + +- (NSInteger)cookieStorageMethod { + return cookieStorageMethod_; +} + +- (void)setCookieStorageMethod:(NSInteger)method { + + cookieStorageMethod_ = method; + + if (method == kGTMHTTPFetcherCookieStorageMethodSystemDefault) { + // System default + [request_ setHTTPShouldHandleCookies:YES]; + + // No need for a cookie storage object + self.cookieStorage = nil; + + } else { + // Not system default + [request_ setHTTPShouldHandleCookies:NO]; + + if (method == kGTMHTTPFetcherCookieStorageMethodStatic) { + // Store cookies in the static array + NSAssert(gGTMFetcherStaticCookieStorage != nil, + @"cookie storage requires GTMHTTPFetchHistory"); + + self.cookieStorage = gGTMFetcherStaticCookieStorage; + } else if (method == kGTMHTTPFetcherCookieStorageMethodFetchHistory) { + // store cookies in the fetch history + self.cookieStorage = [fetchHistory_ cookieStorage]; + } else { + // kGTMHTTPFetcherCookieStorageMethodNone - ignore cookies + self.cookieStorage = nil; + } + } +} + ++ (id <GTMCookieStorageProtocol>)staticCookieStorage { + return gGTMFetcherStaticCookieStorage; +} + ++ (BOOL)doesSupportSentDataCallback { +#if GTM_IPHONE + // NSURLConnection's didSendBodyData: delegate support appears to be + // available starting in iPhone OS 3.0 + return (NSFoundationVersionNumber >= 678.47); +#else + // Per WebKit's MaxFoundationVersionWithoutdidSendBodyDataDelegate + // + // Indicates if NSURLConnection will invoke the didSendBodyData: delegate + // method + return (NSFoundationVersionNumber > 677.21); +#endif +} + +- (id <GTMHTTPFetchHistoryProtocol>)fetchHistory { + return fetchHistory_; +} + +- (void)setFetchHistory:(id <GTMHTTPFetchHistoryProtocol>)fetchHistory { + [fetchHistory_ autorelease]; + fetchHistory_ = [fetchHistory retain]; + + if (fetchHistory_ != nil) { + // set the fetch history's cookie array to be the cookie store + [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodFetchHistory]; + + } else { + // The fetch history was removed + if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory) { + // Fall back to static storage + [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic]; + } + } +} + +- (id)userData { + @synchronized(self) { + return userData_; + } +} + +- (void)setUserData:(id)theObj { + @synchronized(self) { + [userData_ autorelease]; + userData_ = [theObj retain]; + } +} + +- (void)setProperties:(NSMutableDictionary *)dict { + @synchronized(self) { + [properties_ autorelease]; + + // This copies rather than retains the parameter for compatiblity with + // an earlier version that took an immutable parameter and copied it. + properties_ = [dict mutableCopy]; + } +} + +- (NSMutableDictionary *)properties { + @synchronized(self) { + return properties_; + } +} + +- (void)setProperty:(id)obj forKey:(NSString *)key { + @synchronized(self) { + if (properties_ == nil && obj != nil) { + [self setProperties:[NSMutableDictionary dictionary]]; + } + [properties_ setValue:obj forKey:key]; + } +} + +- (id)propertyForKey:(NSString *)key { + @synchronized(self) { + return [properties_ objectForKey:key]; + } +} + +- (void)addPropertiesFromDictionary:(NSDictionary *)dict { + @synchronized(self) { + if (properties_ == nil && dict != nil) { + [self setProperties:[[dict mutableCopy] autorelease]]; + } else { + [properties_ addEntriesFromDictionary:dict]; + } + } +} + +- (void)setCommentWithFormat:(id)format, ... { +#if !STRIP_GTM_FETCH_LOGGING + NSString *result = format; + if (format) { + va_list argList; + va_start(argList, format); + + result = [[[NSString alloc] initWithFormat:format + arguments:argList] autorelease]; + va_end(argList); + } + [self setComment:result]; +#endif +} + ++ (Class)connectionClass { + if (gGTMFetcherConnectionClass == nil) { + gGTMFetcherConnectionClass = [NSURLConnection class]; + } + return gGTMFetcherConnectionClass; +} + ++ (void)setConnectionClass:(Class)theClass { + gGTMFetcherConnectionClass = theClass; +} + +#if STRIP_GTM_FETCH_LOGGING ++ (void)setLoggingEnabled:(BOOL)flag { +} +#endif // STRIP_GTM_FETCH_LOGGING + +@end + +void GTMAssertSelectorNilOrImplementedWithArgs(id obj, SEL sel, ...) { + + // Verify that the object's selector is implemented with the proper + // number and type of arguments +#if DEBUG + va_list argList; + va_start(argList, sel); + + if (obj && sel) { + // Check that the selector is implemented + if (![obj respondsToSelector:sel]) { + NSLog(@"\"%@\" selector \"%@\" is unimplemented or misnamed", + NSStringFromClass([obj class]), + NSStringFromSelector(sel)); + NSCAssert(0, @"callback selector unimplemented or misnamed"); + } else { + const char *expectedArgType; + unsigned int argCount = 2; // skip self and _cmd + NSMethodSignature *sig = [obj methodSignatureForSelector:sel]; + + // Check that each expected argument is present and of the correct type + while ((expectedArgType = va_arg(argList, const char*)) != 0) { + + if ([sig numberOfArguments] > argCount) { + const char *foundArgType = [sig getArgumentTypeAtIndex:argCount]; + + if(0 != strncmp(foundArgType, expectedArgType, strlen(expectedArgType))) { + NSLog(@"\"%@\" selector \"%@\" argument %d should be type %s", + NSStringFromClass([obj class]), + NSStringFromSelector(sel), (argCount - 2), expectedArgType); + NSCAssert(0, @"callback selector argument type mistake"); + } + } + argCount++; + } + + // Check that the proper number of arguments are present in the selector + if (argCount != [sig numberOfArguments]) { + NSLog( @"\"%@\" selector \"%@\" should have %d arguments", + NSStringFromClass([obj class]), + NSStringFromSelector(sel), (argCount - 2)); + NSCAssert(0, @"callback selector arguments incorrect"); + } + } + } + + va_end(argList); +#endif +} + +NSString *GTMCleanedUserAgentString(NSString *str) { + // Reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html + // and http://www-archive.mozilla.org/build/user-agent-strings.html + + if (str == nil) return nil; + + NSMutableString *result = [NSMutableString stringWithString:str]; + + // Replace spaces with underscores + [result replaceOccurrencesOfString:@" " + withString:@"_" + options:0 + range:NSMakeRange(0, [result length])]; + + // Delete http token separators and remaining whitespace + static NSCharacterSet *charsToDelete = nil; + if (charsToDelete == nil) { + // Make a set of unwanted characters + NSString *const kSeparators = @"()<>@,;:\\\"/[]?={}"; + + NSMutableCharacterSet *mutableChars; + mutableChars = [[[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy] autorelease]; + [mutableChars addCharactersInString:kSeparators]; + charsToDelete = [mutableChars copy]; // hang on to an immutable copy + } + + while (1) { + NSRange separatorRange = [result rangeOfCharacterFromSet:charsToDelete]; + if (separatorRange.location == NSNotFound) break; + + [result deleteCharactersInRange:separatorRange]; + }; + + return result; +} + +NSString *GTMSystemVersionString(void) { + NSString *systemString = @""; + +#if TARGET_OS_MAC && !TARGET_OS_IPHONE + // Mac build + static NSString *savedSystemString = nil; + if (savedSystemString == nil) { + // With Gestalt inexplicably deprecated in 10.8, we're reduced to reading + // the system plist file. + NSString *const kPath = @"/System/Library/CoreServices/SystemVersion.plist"; + NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:kPath]; + NSString *versString = [plist objectForKey:@"ProductVersion"]; + if ([versString length] == 0) { + versString = @"10.?.?"; + } + savedSystemString = [[NSString alloc] initWithFormat:@"MacOSX/%@", versString]; + } + systemString = savedSystemString; +#elif TARGET_OS_IPHONE + // Compiling against the iPhone SDK + + static NSString *savedSystemString = nil; + if (savedSystemString == nil) { + // Avoid the slowness of calling currentDevice repeatedly on the iPhone + UIDevice* currentDevice = [UIDevice currentDevice]; + + NSString *rawModel = [currentDevice model]; + NSString *model = GTMCleanedUserAgentString(rawModel); + + NSString *systemVersion = [currentDevice systemVersion]; + + savedSystemString = [[NSString alloc] initWithFormat:@"%@/%@", + model, systemVersion]; // "iPod_Touch/2.2" + } + systemString = savedSystemString; + +#elif (GTL_IPHONE || GDATA_IPHONE) + // Compiling iOS libraries against the Mac SDK + systemString = @"iPhone/x.x"; + +#elif defined(_SYS_UTSNAME_H) + // Foundation-only build + struct utsname unameRecord; + uname(&unameRecord); + + systemString = [NSString stringWithFormat:@"%s/%s", + unameRecord.sysname, unameRecord.release]; // "Darwin/8.11.1" +#endif + + return systemString; +} + +// Return a generic name and version for the current application; this avoids +// anonymous server transactions. +NSString *GTMApplicationIdentifier(NSBundle *bundle) { + static NSString *sAppID = nil; + if (sAppID != nil) return sAppID; + + // If there's a bundle ID, use that; otherwise, use the process name + if (bundle == nil) { + bundle = [NSBundle mainBundle]; + } + + NSString *identifier; + NSString *bundleID = [bundle bundleIdentifier]; + if ([bundleID length] > 0) { + identifier = bundleID; + } else { + // Fall back on the procname, prefixed by "proc" to flag that it's + // autogenerated and perhaps unreliable + NSString *procName = [[NSProcessInfo processInfo] processName]; + identifier = [NSString stringWithFormat:@"proc_%@", procName]; + } + + // Clean up whitespace and special characters + identifier = GTMCleanedUserAgentString(identifier); + + // If there's a version number, append that + NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + if ([version length] == 0) { + version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"]; + } + + // Clean up whitespace and special characters + version = GTMCleanedUserAgentString(version); + + // Glue the two together (cleanup done above or else cleanup would strip the + // slash) + if ([version length] > 0) { + identifier = [identifier stringByAppendingFormat:@"/%@", version]; + } + + sAppID = [identifier copy]; + return sAppID; +} diff --git a/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.h b/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.h new file mode 100644 index 00000000..8703164b --- /dev/null +++ b/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.h @@ -0,0 +1,356 @@ +/* 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 kGTMOAuth2ErrorObjectKey _INITIALIZE_AS(@"kGTMOAuth2ErrorObjectKey"); + +_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"); +_EXTERN NSString* const kGTMOAuth2AccessTokenRefreshFailed _INITIALIZE_AS(@"kGTMOAuth2AccessTokenRefreshFailed"); + +// 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_; + + NSString *authorizationTokenKey_; + + NSDictionary *additionalTokenRequestParameters_; + NSDictionary *additionalGrantTypeRequestParameters_; + + // 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; +@property (retain) NSString *refreshScope; + +// Apps may optionally add parameters here to be provided to the token +// endpoint on token requests and refreshes. +@property (retain) NSDictionary *additionalTokenRequestParameters; + +// Apps may optionally add parameters here to be provided to the token +// endpoint on specific token requests and refreshes, keyed by the grant_type. +// For example, if a different "type" parameter is required for obtaining +// the auth code and on refresh, this might be: +// +// viewController.authentication.additionalGrantTypeRequestParameters = @{ +// @"authorization_code" : @{ @"type" : @"code" }, +// @"refresh_token" : @{ @"type" : @"refresh" } +// }; +@property (retain) NSDictionary *additionalGrantTypeRequestParameters; + +// 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 ID; not used for authentication +@property (retain) NSString *userID; + +// 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 or access 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; + +// Key for the response parameter used for the authorization header; by default, +// "access_token" is used, but some servers may expect alternatives, like +// "id_token". +@property (copy) NSString *authorizationTokenKey; + +// 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. This will also cancel the authorization +// for all fetch requests pending authorization. +- (void)stopAuthorization; + +// Prevents authorization callback for a given request. +- (void)stopAuthorizationForRequest:(NSURLRequest *)request; + +// 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; + ++ (NSDictionary *)dictionaryWithJSONData:(NSData *)data; + ++ (NSString *)scopeWithStrings:(NSString *)firsStr, ... NS_REQUIRES_NIL_TERMINATION; +@end + +#endif // GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.m b/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.m new file mode 100644 index 00000000..b0c99776 --- /dev/null +++ b/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.m @@ -0,0 +1,1275 @@ +/* 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 GTMOAUTH2AUTHENTICATION_DEFINE_GLOBALS 1 +#import "GTMOAuth2Authentication.h" + +// standard OAuth keys +static NSString *const kOAuth2AccessTokenKey = @"access_token"; +static NSString *const kOAuth2RefreshTokenKey = @"refresh_token"; +static NSString *const kOAuth2ClientIDKey = @"client_id"; +static NSString *const kOAuth2ClientSecretKey = @"client_secret"; +static NSString *const kOAuth2RedirectURIKey = @"redirect_uri"; +static NSString *const kOAuth2ResponseTypeKey = @"response_type"; +static NSString *const kOAuth2ScopeKey = @"scope"; +static NSString *const kOAuth2ErrorKey = @"error"; +static NSString *const kOAuth2TokenTypeKey = @"token_type"; +static NSString *const kOAuth2ExpiresInKey = @"expires_in"; +static NSString *const kOAuth2CodeKey = @"code"; +static NSString *const kOAuth2AssertionKey = @"assertion"; +static NSString *const kOAuth2RefreshScopeKey = @"refreshScope"; + +// additional persistent keys +static NSString *const kServiceProviderKey = @"serviceProvider"; +static NSString *const kUserIDKey = @"userID"; +static NSString *const kUserEmailKey = @"email"; +static NSString *const kUserEmailIsVerifiedKey = @"isVerified"; + +// fetcher keys +static NSString *const kTokenFetchDelegateKey = @"delegate"; +static NSString *const kTokenFetchSelectorKey = @"sel"; + +static NSString *const kRefreshFetchArgsKey = @"requestArgs"; + +// If GTMNSJSONSerialization is available, it is used for formatting JSON +#if (TARGET_OS_MAC && !TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED < 1070)) || \ + (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED < 50000)) +@interface GTMNSJSONSerialization : NSObject ++ (id)JSONObjectWithData:(NSData *)data options:(NSUInteger)opt error:(NSError **)error; +@end +#endif + +@interface GTMOAuth2ParserClass : NSObject +// just enough of SBJSON to be able to parse +- (id)objectWithString:(NSString*)repr error:(NSError**)error; +@end + +// wrapper class for requests needing authorization and their callbacks +@interface GTMOAuth2AuthorizationArgs : NSObject { + @private + NSMutableURLRequest *request_; + id delegate_; + SEL sel_; + id completionHandler_; + NSThread *thread_; + NSError *error_; +} + +@property (retain) NSMutableURLRequest *request; +@property (retain) id delegate; +@property (assign) SEL selector; +@property (copy) id completionHandler; +@property (retain) NSThread *thread; +@property (retain) NSError *error; + ++ (GTMOAuth2AuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req + delegate:(id)delegate + selector:(SEL)sel + completionHandler:(id)completionHandler + thread:(NSThread *)thread; +@end + +@implementation GTMOAuth2AuthorizationArgs + +@synthesize request = request_, + delegate = delegate_, + selector = sel_, + completionHandler = completionHandler_, + thread = thread_, + error = error_; + ++ (GTMOAuth2AuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req + delegate:(id)delegate + selector:(SEL)sel + completionHandler:(id)completionHandler + thread:(NSThread *)thread { + GTMOAuth2AuthorizationArgs *obj; + obj = [[[GTMOAuth2AuthorizationArgs alloc] init] autorelease]; + obj.request = req; + obj.delegate = delegate; + obj.selector = sel; + obj.completionHandler = completionHandler; + obj.thread = thread; + return obj; +} + +- (void)dealloc { + [request_ release]; + [delegate_ release]; + [completionHandler_ release]; + [thread_ release]; + [error_ release]; + + [super dealloc]; +} +@end + + +@interface GTMOAuth2Authentication () + +@property (retain) NSMutableArray *authorizationQueue; +@property (readonly) NSString *authorizationToken; + +- (void)setKeysForResponseJSONData:(NSData *)data; + +- (BOOL)authorizeRequestArgs:(GTMOAuth2AuthorizationArgs *)args; + +- (BOOL)authorizeRequestImmediateArgs:(GTMOAuth2AuthorizationArgs *)args; + +- (BOOL)shouldRefreshAccessToken; + +- (void)updateExpirationDate; + +- (void)tokenFetcher:(GTMHTTPFetcher *)fetcher + finishedWithData:(NSData *)data + error:(NSError *)error; + +- (void)auth:(GTMOAuth2Authentication *)auth +finishedRefreshWithFetcher:(GTMHTTPFetcher *)fetcher + error:(NSError *)error; + +- (void)invokeCallbackArgs:(GTMOAuth2AuthorizationArgs *)args; + ++ (void)invokeDelegate:(id)delegate + selector:(SEL)sel + object:(id)obj1 + object:(id)obj2 + object:(id)obj3; + ++ (NSString *)unencodedOAuthParameterForString:(NSString *)str; ++ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict; + ++ (NSDictionary *)dictionaryWithResponseData:(NSData *)data; + +@end + +@implementation GTMOAuth2Authentication + +@synthesize clientID = clientID_, + clientSecret = clientSecret_, + redirectURI = redirectURI_, + parameters = parameters_, + authorizationTokenKey = authorizationTokenKey_, + tokenURL = tokenURL_, + expirationDate = expirationDate_, + additionalTokenRequestParameters = additionalTokenRequestParameters_, + additionalGrantTypeRequestParameters = additionalGrantTypeRequestParameters_, + refreshFetcher = refreshFetcher_, + fetcherService = fetcherService_, + parserClass = parserClass_, + shouldAuthorizeAllRequests = shouldAuthorizeAllRequests_, + userData = userData_, + properties = properties_, + authorizationQueue = authorizationQueue_; + +// Response parameters +@dynamic accessToken, + refreshToken, + code, + assertion, + refreshScope, + errorString, + tokenType, + scope, + expiresIn, + serviceProvider, + userEmail, + userEmailIsVerified; + +@dynamic canAuthorize; + ++ (id)authenticationWithServiceProvider:(NSString *)serviceProvider + tokenURL:(NSURL *)tokenURL + redirectURI:(NSString *)redirectURI + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret { + GTMOAuth2Authentication *obj = [[[self alloc] init] autorelease]; + obj.serviceProvider = serviceProvider; + obj.tokenURL = tokenURL; + obj.redirectURI = redirectURI; + obj.clientID = clientID; + obj.clientSecret = clientSecret; + return obj; +} + +- (id)init { + self = [super init]; + if (self) { + authorizationQueue_ = [[NSMutableArray alloc] init]; + parameters_ = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (NSString *)description { + NSArray *props = [NSArray arrayWithObjects:@"accessToken", @"refreshToken", + @"code", @"assertion", @"expirationDate", @"errorString", + nil]; + NSMutableString *valuesStr = [NSMutableString string]; + NSString *separator = @""; + for (NSString *prop in props) { + id result = [self valueForKey:prop]; + if (result) { + [valuesStr appendFormat:@"%@%@=\"%@\"", separator, prop, result]; + separator = @", "; + } + } + + return [NSString stringWithFormat:@"%@ %p: {%@}", + [self class], self, valuesStr]; +} + +- (void)dealloc { + [clientID_ release]; + [clientSecret_ release]; + [redirectURI_ release]; + [parameters_ release]; + [authorizationTokenKey_ release]; + [tokenURL_ release]; + [expirationDate_ release]; + [additionalTokenRequestParameters_ release]; + [additionalGrantTypeRequestParameters_ release]; + [refreshFetcher_ release]; + [authorizationQueue_ release]; + [userData_ release]; + [properties_ release]; + + [super dealloc]; +} + +#pragma mark - + +- (void)setKeysForResponseDictionary:(NSDictionary *)dict { + if (dict == nil) return; + + // If a new code or access token is being set, remove the old expiration + NSString *newCode = [dict objectForKey:kOAuth2CodeKey]; + NSString *newAccessToken = [dict objectForKey:kOAuth2AccessTokenKey]; + if (newCode || newAccessToken) { + self.expiresIn = nil; + } + + BOOL didRefreshTokenChange = NO; + NSString *refreshToken = [dict objectForKey:kOAuth2RefreshTokenKey]; + if (refreshToken) { + NSString *priorRefreshToken = self.refreshToken; + + if (priorRefreshToken != refreshToken + && (priorRefreshToken == nil + || ![priorRefreshToken isEqual:refreshToken])) { + didRefreshTokenChange = YES; + } + } + + [self.parameters addEntriesFromDictionary:dict]; + [self updateExpirationDate]; + + if (didRefreshTokenChange) { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName:kGTMOAuth2RefreshTokenChanged + object:self + userInfo:nil]; + } + // NSLog(@"keys set ----------------------------\n%@", dict); +} + +- (void)setKeysForResponseString:(NSString *)str { + NSDictionary *dict = [[self class] dictionaryWithResponseString:str]; + [self setKeysForResponseDictionary:dict]; +} + +- (void)setKeysForResponseJSONData:(NSData *)data { + NSDictionary *dict = [[self class] dictionaryWithJSONData:data]; + [self setKeysForResponseDictionary:dict]; +} + ++ (NSDictionary *)dictionaryWithJSONData:(NSData *)data { + NSMutableDictionary *obj = nil; + NSError *error = nil; + + Class serializer = NSClassFromString(@"NSJSONSerialization"); + if (serializer) { + const NSUInteger kOpts = (1UL << 0); // NSJSONReadingMutableContainers + obj = [serializer JSONObjectWithData:data + options:kOpts + error:&error]; +#if DEBUG + if (error) { + NSString *str = [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] autorelease]; + NSLog(@"NSJSONSerialization error %@ parsing %@", + error, str); + } +#endif + return obj; + } else { + // try SBJsonParser or SBJSON + Class jsonParseClass = NSClassFromString(@"SBJsonParser"); + if (!jsonParseClass) { + jsonParseClass = NSClassFromString(@"SBJSON"); + } + if (jsonParseClass) { + GTMOAuth2ParserClass *parser = [[[jsonParseClass alloc] init] autorelease]; + NSString *jsonStr = [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] autorelease]; + if (jsonStr) { + obj = [parser objectWithString:jsonStr error:&error]; +#if DEBUG + if (error) { + NSLog(@"%@ error %@ parsing %@", NSStringFromClass(jsonParseClass), + error, jsonStr); + } +#endif + return obj; + } + } else { +#if DEBUG + NSAssert(0, @"GTMOAuth2Authentication: No parser available"); +#endif + } + } + return nil; +} + +#pragma mark Authorizing Requests + +// General entry point for authorizing requests + +#if NS_BLOCKS_AVAILABLE +// Authorizing with a completion block +- (void)authorizeRequest:(NSMutableURLRequest *)request + completionHandler:(void (^)(NSError *error))handler { + + GTMOAuth2AuthorizationArgs *args; + args = [GTMOAuth2AuthorizationArgs argsWithRequest:request + delegate:nil + selector:NULL + completionHandler:handler + thread:[NSThread currentThread]]; + [self authorizeRequestArgs:args]; +} +#endif + +// Authorizing with a callback selector +// +// Selector has the signature +// - (void)authentication:(GTMOAuth2Authentication *)auth +// request:(NSMutableURLRequest *)request +// finishedWithError:(NSError *)error; +- (void)authorizeRequest:(NSMutableURLRequest *)request + delegate:(id)delegate + didFinishSelector:(SEL)sel { + GTMAssertSelectorNilOrImplementedWithArgs(delegate, sel, + @encode(GTMOAuth2Authentication *), + @encode(NSMutableURLRequest *), + @encode(NSError *), 0); + + GTMOAuth2AuthorizationArgs *args; + args = [GTMOAuth2AuthorizationArgs argsWithRequest:request + delegate:delegate + selector:sel + completionHandler:nil + thread:[NSThread currentThread]]; + [self authorizeRequestArgs:args]; +} + +// Internal routine common to delegate and block invocations +- (BOOL)authorizeRequestArgs:(GTMOAuth2AuthorizationArgs *)args { + BOOL didAttempt = NO; + + @synchronized(authorizationQueue_) { + + BOOL shouldRefresh = [self shouldRefreshAccessToken]; + + if (shouldRefresh) { + // attempt to refresh now; once we have a fresh access token, we will + // authorize the request and call back to the user + didAttempt = YES; + + if (self.refreshFetcher == nil) { + // there's not already a refresh pending + SEL finishedSel = @selector(auth:finishedRefreshWithFetcher:error:); + self.refreshFetcher = [self beginTokenFetchWithDelegate:self + didFinishSelector:finishedSel]; + if (self.refreshFetcher) { + [authorizationQueue_ addObject:args]; + } + } else { + // there's already a refresh pending + [authorizationQueue_ addObject:args]; + } + } + + if (!shouldRefresh || self.refreshFetcher == nil) { + // we're not fetching a new access token, so we can authorize the request + // now + didAttempt = [self authorizeRequestImmediateArgs:args]; + } + } + return didAttempt; +} + +- (void)auth:(GTMOAuth2Authentication *)auth +finishedRefreshWithFetcher:(GTMHTTPFetcher *)fetcher + error:(NSError *)error { + @synchronized(authorizationQueue_) { + // If there's an error, we want to try using the old access token anyway, + // in case it's a backend problem preventing refresh, in which case + // access tokens past their expiration date may still work + + self.refreshFetcher = nil; + + // Swap in a new auth queue in case the callbacks try to immediately auth + // another request + NSArray *pendingAuthQueue = [NSArray arrayWithArray:authorizationQueue_]; + [authorizationQueue_ removeAllObjects]; + + BOOL hasAccessToken = ([self.accessToken length] > 0); + + NSString *noteName; + NSDictionary *userInfo = nil; + if (hasAccessToken && error == nil) { + // Successful refresh. + noteName = kGTMOAuth2AccessTokenRefreshed; + userInfo = nil; + } else { + // Google's OAuth 2 implementation returns a 400 with JSON body + // containing error key "invalid_grant" to indicate the refresh token + // is invalid or has been revoked by the user. We'll promote the + // JSON error key's value for easy inspection by the observer. + noteName = kGTMOAuth2AccessTokenRefreshFailed; + NSString *jsonErr = nil; + if ([error code] == kGTMHTTPFetcherStatusBadRequest) { + NSDictionary *json = [[error userInfo] objectForKey:kGTMOAuth2ErrorJSONKey]; + jsonErr = [json objectForKey:kGTMOAuth2ErrorMessageKey]; + } + // error and jsonErr may be nil + userInfo = [NSMutableDictionary dictionary]; + [userInfo setValue:error forKey:kGTMOAuth2ErrorObjectKey]; + [userInfo setValue:jsonErr forKey:kGTMOAuth2ErrorMessageKey]; + } + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName:noteName + object:self + userInfo:userInfo]; + + for (GTMOAuth2AuthorizationArgs *args in pendingAuthQueue) { + if (!hasAccessToken && args.error == nil) { + args.error = error; + } + + [self authorizeRequestImmediateArgs:args]; + } + } +} + +- (BOOL)isAuthorizingRequest:(NSURLRequest *)request { + BOOL wasFound = NO; + @synchronized(authorizationQueue_) { + for (GTMOAuth2AuthorizationArgs *args in authorizationQueue_) { + if ([args request] == request) { + wasFound = YES; + break; + } + } + } + return wasFound; +} + +- (BOOL)isAuthorizedRequest:(NSURLRequest *)request { + NSString *authStr = [request valueForHTTPHeaderField:@"Authorization"]; + return ([authStr length] > 0); +} + +- (void)stopAuthorization { + @synchronized(authorizationQueue_) { + [authorizationQueue_ removeAllObjects]; + + [self.refreshFetcher stopFetching]; + self.refreshFetcher = nil; + } +} + +- (void)stopAuthorizationForRequest:(NSURLRequest *)request { + @synchronized(authorizationQueue_) { + NSUInteger argIndex = 0; + BOOL found = NO; + for (GTMOAuth2AuthorizationArgs *args in authorizationQueue_) { + if ([args request] == request) { + found = YES; + break; + } + argIndex++; + } + + if (found) { + [authorizationQueue_ removeObjectAtIndex:argIndex]; + + // If the queue is now empty, go ahead and stop the fetcher. + if ([authorizationQueue_ count] == 0) { + [self stopAuthorization]; + } + } + } +} + +- (BOOL)authorizeRequestImmediateArgs:(GTMOAuth2AuthorizationArgs *)args { + // This authorization entry point never attempts to refresh the access token, + // but does call the completion routine + + NSMutableURLRequest *request = args.request; + + NSString *scheme = [[request URL] scheme]; + BOOL isAuthorizableRequest = self.shouldAuthorizeAllRequests + || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame; + if (!isAuthorizableRequest) { + // Request is not https, so may be insecure + // + // The NSError will be created below +#if DEBUG + NSLog(@"Cannot authorize request with scheme %@ (%@)", scheme, request); +#endif + } + + // Get the access token. + NSString *accessToken = self.authorizationToken; + if (isAuthorizableRequest && [accessToken length] > 0) { + if (request) { + // we have a likely valid access token + NSString *value = [NSString stringWithFormat:@"%s %@", + GTM_OAUTH2_BEARER, accessToken]; + [request setValue:value forHTTPHeaderField:@"Authorization"]; + } + + // We've authorized the request, even if the previous refresh + // failed with an error + args.error = nil; + } else if (args.error == nil) { + NSDictionary *userInfo = nil; + if (request) { + userInfo = [NSDictionary dictionaryWithObject:request + forKey:kGTMOAuth2ErrorRequestKey]; + } + NSInteger code = (isAuthorizableRequest ? + kGTMOAuth2ErrorAuthorizationFailed : + kGTMOAuth2ErrorUnauthorizableRequest); + args.error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain + code:code + userInfo:userInfo]; + } + + // Invoke any callbacks on the proper thread + if (args.delegate || args.completionHandler) { + NSThread *targetThread = args.thread; + BOOL isSameThread = [targetThread isEqual:[NSThread currentThread]]; + + if (isSameThread) { + [self invokeCallbackArgs:args]; + } else { + SEL sel = @selector(invokeCallbackArgs:); + NSOperationQueue *delegateQueue = self.fetcherService.delegateQueue; + if (delegateQueue) { + NSInvocationOperation *op; + op = [[[NSInvocationOperation alloc] initWithTarget:self + selector:sel + object:args] autorelease]; + [delegateQueue addOperation:op]; + } else { + [self performSelector:sel + onThread:targetThread + withObject:args + waitUntilDone:NO]; + } + } + } + + BOOL didAuth = (args.error == nil); + return didAuth; +} + +- (void)invokeCallbackArgs:(GTMOAuth2AuthorizationArgs *)args { + // Invoke the callbacks + NSError *error = args.error; + + id delegate = args.delegate; + SEL sel = args.selector; + if (delegate && sel) { + NSMutableURLRequest *request = args.request; + + NSMethodSignature *sig = [delegate methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:delegate]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&request atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + } + +#if NS_BLOCKS_AVAILABLE + id handler = args.completionHandler; + if (handler) { + void (^authCompletionBlock)(NSError *) = handler; + authCompletionBlock(error); + } +#endif +} + +- (BOOL)authorizeRequest:(NSMutableURLRequest *)request { + // Entry point for synchronous authorization mechanisms + GTMOAuth2AuthorizationArgs *args; + args = [GTMOAuth2AuthorizationArgs argsWithRequest:request + delegate:nil + selector:NULL + completionHandler:nil + thread:[NSThread currentThread]]; + return [self authorizeRequestImmediateArgs:args]; +} + +- (BOOL)canAuthorize { + NSString *token = self.refreshToken; + if (token == nil) { + // For services which do not support refresh tokens, we'll just check + // the access token. + token = self.authorizationToken; + } + BOOL canAuth = [token length] > 0; + return canAuth; +} + +- (BOOL)shouldRefreshAccessToken { + // We should refresh the access token when it's missing or nearly expired + // and we have a refresh token + BOOL shouldRefresh = NO; + NSString *accessToken = self.accessToken; + NSString *refreshToken = self.refreshToken; + NSString *assertion = self.assertion; + NSString *code = self.code; + + BOOL hasRefreshToken = ([refreshToken length] > 0); + BOOL hasAccessToken = ([accessToken length] > 0); + BOOL hasAssertion = ([assertion length] > 0); + BOOL hasCode = ([code length] > 0); + + // Determine if we need to refresh the access token + if (hasRefreshToken || hasAssertion || hasCode) { + if (!hasAccessToken) { + shouldRefresh = YES; + } else { + // We'll consider the token expired if it expires 60 seconds from now + // or earlier + NSDate *expirationDate = self.expirationDate; + NSTimeInterval timeToExpire = [expirationDate timeIntervalSinceNow]; + if (expirationDate == nil || timeToExpire < 60.0) { + // access token has expired, or will in a few seconds + shouldRefresh = YES; + } + } + } + return shouldRefresh; +} + +- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds { + // If there is a refresh fetcher pending, wait for it. + // + // This is only intended for unit test or for use in command-line tools. + GTMHTTPFetcher *fetcher = self.refreshFetcher; + [fetcher waitForCompletionWithTimeout:timeoutInSeconds]; +} + +#pragma mark Token Fetch + +- (NSString *)userAgent { + NSBundle *bundle = [NSBundle mainBundle]; + NSString *appID = [bundle bundleIdentifier]; + + NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + if (version == nil) { + version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"]; + } + + if (appID && version) { + appID = [appID stringByAppendingFormat:@"/%@", version]; + } + + NSString *userAgent = @"gtm-oauth2"; + if (appID) { + userAgent = [userAgent stringByAppendingFormat:@" %@", appID]; + } + return userAgent; +} + +- (GTMHTTPFetcher *)beginTokenFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSel { + + NSMutableDictionary *paramsDict = [NSMutableDictionary dictionary]; + + NSString *fetchType; + + NSString *refreshToken = self.refreshToken; + NSString *code = self.code; + NSString *assertion = self.assertion; + NSString *grantType = nil; + + if (refreshToken) { + // We have a refresh token + grantType = @"refresh_token"; + [paramsDict setObject:refreshToken forKey:@"refresh_token"]; + + NSString *refreshScope = self.refreshScope; + if ([refreshScope length] > 0) { + [paramsDict setObject:refreshScope forKey:@"scope"]; + } + + fetchType = kGTMOAuth2FetchTypeRefresh; + } else if (code) { + // We have a code string + grantType = @"authorization_code"; + [paramsDict setObject:code forKey:@"code"]; + + NSString *redirectURI = self.redirectURI; + if ([redirectURI length] > 0) { + [paramsDict setObject:redirectURI forKey:@"redirect_uri"]; + } + + NSString *scope = self.scope; + if ([scope length] > 0) { + [paramsDict setObject:scope forKey:@"scope"]; + } + + fetchType = kGTMOAuth2FetchTypeToken; + } else if (assertion) { + // We have an assertion string + grantType = @"http://oauth.net/grant_type/jwt/1.0/bearer"; + [paramsDict setObject:assertion forKey:@"assertion"]; + fetchType = kGTMOAuth2FetchTypeAssertion; + } else { +#if DEBUG + NSAssert(0, @"unexpected lack of code or refresh token for fetching"); +#endif + return nil; + } + [paramsDict setObject:grantType forKey:@"grant_type"]; + + NSString *clientID = self.clientID; + if ([clientID length] > 0) { + [paramsDict setObject:clientID forKey:@"client_id"]; + } + + NSString *clientSecret = self.clientSecret; + if ([clientSecret length] > 0) { + [paramsDict setObject:clientSecret forKey:@"client_secret"]; + } + + NSDictionary *additionalParams = self.additionalTokenRequestParameters; + if (additionalParams) { + [paramsDict addEntriesFromDictionary:additionalParams]; + } + NSDictionary *grantTypeParams = + [self.additionalGrantTypeRequestParameters objectForKey:grantType]; + if (grantTypeParams) { + [paramsDict addEntriesFromDictionary:grantTypeParams]; + } + + NSString *paramStr = [[self class] encodedQueryParametersForDictionary:paramsDict]; + NSData *paramData = [paramStr dataUsingEncoding:NSUTF8StringEncoding]; + + NSURL *tokenURL = self.tokenURL; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:tokenURL]; + [request setValue:@"application/x-www-form-urlencoded" + forHTTPHeaderField:@"Content-Type"]; + + NSString *userAgent = [self userAgent]; + [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + + GTMHTTPFetcher *fetcher; + id <GTMHTTPFetcherServiceProtocol> fetcherService = self.fetcherService; + if (fetcherService) { + fetcher = [fetcherService fetcherWithRequest:request]; + + // Don't use an authorizer for an auth token fetch + fetcher.authorizer = nil; + } else { + fetcher = [GTMHTTPFetcher fetcherWithRequest:request]; + } + + NSString *const template = (refreshToken ? @"refresh token for %@ %@" : @"fetch tokens for %@ %@"); + [fetcher setCommentWithFormat:template, [tokenURL host], [self userEmail]]; + fetcher.postData = paramData; + fetcher.retryEnabled = YES; + fetcher.maxRetryInterval = 15.0; + + // Fetcher properties will retain the delegate + [fetcher setProperty:delegate forKey:kTokenFetchDelegateKey]; + if (finishedSel) { + NSString *selStr = NSStringFromSelector(finishedSel); + [fetcher setProperty:selStr forKey:kTokenFetchSelectorKey]; + } + + if ([fetcher beginFetchWithDelegate:self + didFinishSelector:@selector(tokenFetcher:finishedWithData:error:)]) { + // Fetch began + [self notifyFetchIsRunning:YES fetcher:fetcher type:fetchType]; + return fetcher; + } else { + // Failed to start fetching; typically a URL issue + NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain + code:-1 + userInfo:nil]; + [[self class] invokeDelegate:delegate + selector:finishedSel + object:self + object:nil + object:error]; + return nil; + } +} + +- (void)tokenFetcher:(GTMHTTPFetcher *)fetcher + finishedWithData:(NSData *)data + error:(NSError *)error { + [self notifyFetchIsRunning:NO fetcher:fetcher type:nil]; + + NSDictionary *responseHeaders = [fetcher responseHeaders]; + NSString *responseType = [responseHeaders valueForKey:@"Content-Type"]; + BOOL isResponseJSON = [responseType hasPrefix:@"application/json"]; + BOOL hasData = ([data length] > 0); + + if (error) { + // Failed. If the error body is JSON, parse it and add it to the error's + // userInfo dictionary. + if (hasData) { + if (isResponseJSON) { + NSDictionary *errorJson = [[self class] dictionaryWithJSONData:data]; + if ([errorJson count] > 0) { +#if DEBUG + NSLog(@"Error %@\nError data:\n%@", error, errorJson); +#endif + // Add the JSON error body to the userInfo of the error + NSMutableDictionary *userInfo; + userInfo = [NSMutableDictionary dictionaryWithObject:errorJson + forKey:kGTMOAuth2ErrorJSONKey]; + NSDictionary *prevUserInfo = [error userInfo]; + if (prevUserInfo) { + [userInfo addEntriesFromDictionary:prevUserInfo]; + } + error = [NSError errorWithDomain:[error domain] + code:[error code] + userInfo:userInfo]; + } + } + } + } else { + // Succeeded; we have the requested token. +#if DEBUG + NSAssert(hasData, @"data missing in token response"); +#endif + + if (hasData) { + if (isResponseJSON) { + [self setKeysForResponseJSONData:data]; + } else { + // Support for legacy token servers that return form-urlencoded data + NSString *dataStr = [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] autorelease]; + [self setKeysForResponseString:dataStr]; + } + +#if DEBUG + // Watch for token exchanges that return a non-bearer or unlabeled token + NSString *tokenType = [self tokenType]; + if (tokenType == nil + || [tokenType caseInsensitiveCompare:@"bearer"] != NSOrderedSame) { + NSLog(@"GTMOAuth2: Unexpected token type: %@", tokenType); + } +#endif + } + } + + id delegate = [fetcher propertyForKey:kTokenFetchDelegateKey]; + SEL sel = NULL; + NSString *selStr = [fetcher propertyForKey:kTokenFetchSelectorKey]; + if (selStr) sel = NSSelectorFromString(selStr); + + [[self class] invokeDelegate:delegate + selector:sel + object:self + object:fetcher + object:error]; + + // Prevent a circular reference from retaining the delegate + [fetcher setProperty:nil forKey:kTokenFetchDelegateKey]; +} + +#pragma mark Fetch Notifications + +- (void)notifyFetchIsRunning:(BOOL)isStarting + fetcher:(GTMHTTPFetcher *)fetcher + type:(NSString *)fetchType { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + + NSString *name = (isStarting ? kGTMOAuth2FetchStarted : kGTMOAuth2FetchStopped); + NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: + fetcher, kGTMOAuth2FetcherKey, + fetchType, kGTMOAuth2FetchTypeKey, // fetchType may be nil + nil]; + [nc postNotificationName:name + object:self + userInfo:dict]; +} + +#pragma mark Persistent Response Strings + +- (void)setKeysForPersistenceResponseString:(NSString *)str { + // All persistence keys can be set directly as if returned by a server + [self setKeysForResponseString:str]; +} + +// This returns a "response string" that can be passed later to +// setKeysForResponseString: to reuse an old access token in a new auth object +- (NSString *)persistenceResponseString { + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:4]; + + NSString *refreshToken = self.refreshToken; + NSString *accessToken = nil; + if (refreshToken == nil) { + // We store the access token only for services that do not support refresh + // tokens; otherwise, we assume the access token is too perishable to + // be worth storing + accessToken = self.accessToken; + } + + // Any nil values will not set a dictionary entry + [dict setValue:refreshToken forKey:kOAuth2RefreshTokenKey]; + [dict setValue:accessToken forKey:kOAuth2AccessTokenKey]; + [dict setValue:self.serviceProvider forKey:kServiceProviderKey]; + [dict setValue:self.userID forKey:kUserIDKey]; + [dict setValue:self.userEmail forKey:kUserEmailKey]; + [dict setValue:self.userEmailIsVerified forKey:kUserEmailIsVerifiedKey]; + [dict setValue:self.scope forKey:kOAuth2ScopeKey]; + + NSString *result = [[self class] encodedQueryParametersForDictionary:dict]; + return result; +} + +- (BOOL)primeForRefresh { + if (self.refreshToken == nil) { + // Cannot refresh without a refresh token + return NO; + } + self.accessToken = nil; + self.expiresIn = nil; + self.expirationDate = nil; + self.errorString = nil; + return YES; +} + +- (void)reset { + // Reset all per-authorization values + self.code = nil; + self.accessToken = nil; + self.refreshToken = nil; + self.assertion = nil; + self.expiresIn = nil; + self.errorString = nil; + self.expirationDate = nil; + self.userEmail = nil; + self.userEmailIsVerified = nil; + self.authorizationTokenKey = nil; +} + +#pragma mark Accessors for Response Parameters + +- (NSString *)authorizationToken { + // The token used for authorization is typically the access token unless + // the user has specified that an alternative parameter be used. + NSString *authorizationToken; + NSString *authTokenKey = self.authorizationTokenKey; + if (authTokenKey != nil) { + authorizationToken = [self.parameters objectForKey:authTokenKey]; + } else { + authorizationToken = self.accessToken; + } + return authorizationToken; +} + +- (NSString *)accessToken { + return [self.parameters objectForKey:kOAuth2AccessTokenKey]; +} + +- (void)setAccessToken:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2AccessTokenKey]; +} + +- (NSString *)refreshToken { + return [self.parameters objectForKey:kOAuth2RefreshTokenKey]; +} + +- (void)setRefreshToken:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2RefreshTokenKey]; +} + +- (NSString *)code { + return [self.parameters objectForKey:kOAuth2CodeKey]; +} + +- (void)setCode:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2CodeKey]; +} + +- (NSString *)assertion { + return [self.parameters objectForKey:kOAuth2AssertionKey]; +} + +- (void)setAssertion:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2AssertionKey]; +} + +- (NSString *)refreshScope { + return [self.parameters objectForKey:kOAuth2RefreshScopeKey]; +} + +- (void)setRefreshScope:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2RefreshScopeKey]; +} + +- (NSString *)errorString { + return [self.parameters objectForKey:kOAuth2ErrorKey]; +} + +- (void)setErrorString:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2ErrorKey]; +} + +- (NSString *)tokenType { + return [self.parameters objectForKey:kOAuth2TokenTypeKey]; +} + +- (void)setTokenType:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2TokenTypeKey]; +} + +- (NSString *)scope { + return [self.parameters objectForKey:kOAuth2ScopeKey]; +} + +- (void)setScope:(NSString *)str { + [self.parameters setValue:str forKey:kOAuth2ScopeKey]; +} + +- (NSNumber *)expiresIn { + return [self.parameters objectForKey:kOAuth2ExpiresInKey]; +} + +- (void)setExpiresIn:(NSNumber *)num { + [self.parameters setValue:num forKey:kOAuth2ExpiresInKey]; + [self updateExpirationDate]; +} + +- (void)updateExpirationDate { + // Update our absolute expiration time to something close to when + // the server expects the expiration + NSDate *date = nil; + NSNumber *expiresIn = self.expiresIn; + if (expiresIn) { + unsigned long deltaSeconds = [expiresIn unsignedLongValue]; + if (deltaSeconds > 0) { + date = [NSDate dateWithTimeIntervalSinceNow:deltaSeconds]; + } + } + self.expirationDate = date; +} + +// +// Keys custom to this class, not part of OAuth 2 +// + +- (NSString *)serviceProvider { + return [self.parameters objectForKey:kServiceProviderKey]; +} + +- (void)setServiceProvider:(NSString *)str { + [self.parameters setValue:str forKey:kServiceProviderKey]; +} + +- (NSString *)userID { + return [self.parameters objectForKey:kUserIDKey]; +} + +- (void)setUserID:(NSString *)str { + [self.parameters setValue:str forKey:kUserIDKey]; +} + +- (NSString *)userEmail { + return [self.parameters objectForKey:kUserEmailKey]; +} + +- (void)setUserEmail:(NSString *)str { + [self.parameters setValue:str forKey:kUserEmailKey]; +} + +- (NSString *)userEmailIsVerified { + return [self.parameters objectForKey:kUserEmailIsVerifiedKey]; +} + +- (void)setUserEmailIsVerified:(NSString *)str { + [self.parameters setValue:str forKey:kUserEmailIsVerifiedKey]; +} + +#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 Utility Routines + ++ (NSString *)encodedOAuthValueForString:(NSString *)str { + CFStringRef originalString = (CFStringRef) str; + CFStringRef leaveUnescaped = NULL; + CFStringRef forceEscaped = CFSTR("!*'();:@&=+$,/?%#[]"); + + CFStringRef escapedStr = NULL; + if (str) { + escapedStr = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, + originalString, + leaveUnescaped, + forceEscaped, + kCFStringEncodingUTF8); + [(id)CFMakeCollectable(escapedStr) autorelease]; + } + + return (NSString *)escapedStr; +} + ++ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict { + // Make a string like "cat=fluffy@dog=spot" + NSMutableString *result = [NSMutableString string]; + NSArray *sortedKeys = [[dict allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; + NSString *joiner = @""; + for (NSString *key in sortedKeys) { + NSString *value = [dict objectForKey:key]; + NSString *encodedValue = [self encodedOAuthValueForString:value]; + NSString *encodedKey = [self encodedOAuthValueForString:key]; + [result appendFormat:@"%@%@=%@", joiner, encodedKey, encodedValue]; + joiner = @"&"; + } + return result; +} + ++ (void)invokeDelegate:(id)delegate + selector:(SEL)sel + object:(id)obj1 + object:(id)obj2 + object:(id)obj3 { + if (delegate && sel) { + NSMethodSignature *sig = [delegate methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:delegate]; + [invocation setArgument:&obj1 atIndex:2]; + [invocation setArgument:&obj2 atIndex:3]; + [invocation setArgument:&obj3 atIndex:4]; + [invocation invoke]; + } +} + ++ (NSString *)unencodedOAuthParameterForString:(NSString *)str { + NSString *plainStr = [str stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + return plainStr; +} + ++ (NSDictionary *)dictionaryWithResponseString:(NSString *)responseStr { + // Build a dictionary from a response string of the form + // "cat=fluffy&dog=spot". Missing or empty values are considered + // empty strings; keys and values are percent-decoded. + if (responseStr == nil) return nil; + + NSArray *items = [responseStr componentsSeparatedByString:@"&"]; + + NSMutableDictionary *responseDict = [NSMutableDictionary dictionaryWithCapacity:[items count]]; + + for (NSString *item in items) { + NSString *key = nil; + NSString *value = @""; + + NSRange equalsRange = [item rangeOfString:@"="]; + if (equalsRange.location != NSNotFound) { + // The parameter has at least one '=' + key = [item substringToIndex:equalsRange.location]; + + // There are characters after the '=' + value = [item substringFromIndex:(equalsRange.location + 1)]; + } else { + // The parameter has no '=' + key = item; + } + + NSString *plainKey = [[self class] unencodedOAuthParameterForString:key]; + NSString *plainValue = [[self class] unencodedOAuthParameterForString:value]; + + [responseDict setObject:plainValue forKey:plainKey]; + } + + return responseDict; +} + ++ (NSDictionary *)dictionaryWithResponseData:(NSData *)data { + NSString *responseStr = [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] autorelease]; + NSDictionary *dict = [self dictionaryWithResponseString:responseStr]; + return dict; +} + ++ (NSString *)scopeWithStrings:(NSString *)str, ... { + // concatenate the strings, joined by a single space + NSString *result = @""; + NSString *joiner = @""; + if (str) { + va_list argList; + va_start(argList, str); + while (str) { + result = [result stringByAppendingFormat:@"%@%@", joiner, str]; + joiner = @" "; + str = va_arg(argList, id); + } + va_end(argList); + } + return result; +} + +@end + +#endif // GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.h b/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.h new file mode 100644 index 00000000..ded279bd --- /dev/null +++ b/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.h @@ -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. + */ + +// +// 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" + +@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; + +// Start the sequences for signing in with an authorization code. The +// authentication must contain an authorization code, otherwise the process +// will fail. +- (void)authCodeObtained; + +#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 diff --git a/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.m b/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.m new file mode 100644 index 00000000..d9df97e4 --- /dev/null +++ b/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.m @@ -0,0 +1,939 @@ +/* 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 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)addScopeForGoogleUserInfo; +- (void)fetchGoogleUserInfo; +#endif +- (void)finishSignInWithError:(NSError *)error; + +- (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; ++ (NSData *)decodeWebSafeBase64:(NSString *)base64Str; +#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; +} + +- (void)addScopeForGoogleUserInfo { + 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 + +- (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 + [self addScopeForGoogleUserInfo]; +#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 { + if ([paramStr length] == 0) { + return [NSMutableURLRequest requestWithURL:oldURL]; + } + + 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 +// authCodeObtained 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 authCodeObtained]; + } + // 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 +// authCodeObtained 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 authCodeObtained]; + } + 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)authCodeObtained { + // 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; + + // If the signin was request for exchanging an authentication token to a + // refresh token, there is no window to close. + if (self.webRequestSelector) { + [self closeTheWindow]; + } else { + // For signing in to Google, append the scope for obtaining the + // authenticated user email and profile, as appropriate. This is usually + // done by the startSigningIn method, but this method is not called when + // exchanging an authentication token for a refresh token. +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT + [self addScopeForGoogleUserInfo]; +#endif + } + + 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]; + + if ([auth respondsToSelector:@selector(userAgent)]) { + NSString *userAgent = [auth userAgent]; + [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + } + [request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"]; + + GTMHTTPFetcher *fetcher; + id <GTMHTTPFetcherServiceProtocol> fetcherService = nil; + if ([auth respondsToSelector:@selector(fetcherService)]) { + 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 { + if (!self.shouldFetchGoogleUserProfile) { + // If we only need email and user ID, not the full profile, and we have an + // id_token, it may have the email and user ID so we won't need to fetch + // them. + GTMOAuth2Authentication *auth = self.authentication; + NSString *idToken = [auth.parameters objectForKey:@"id_token"]; + if ([idToken length] > 0) { + // The id_token has three dot-delimited parts. The second is the + // JSON profile. + // + // http://www.tbray.org/ongoing/When/201x/2013/04/04/ID-Tokens + NSArray *parts = [idToken componentsSeparatedByString:@"."]; + if ([parts count] == 3) { + NSString *part2 = [parts objectAtIndex:1]; + if ([part2 length] > 0) { + NSData *data = [[self class] decodeWebSafeBase64:part2]; + if ([data length] > 0) { + [self updateGoogleUserInfoWithData:data]; + if ([[auth userID] length] > 0 && [[auth userEmail] length] > 0) { + // We obtained user ID and email from the ID token. + [self finishSignInWithError:nil]; + return; + } + } + } + } + } + } + + // Fetch the email and profile from the userinfo endpoint. + 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 + [self updateGoogleUserInfoWithData:data]; + } + [self finishSignInWithError:error]; +} + +- (void)updateGoogleUserInfoWithData:(NSData *)data { + if (!data) return; + + GTMOAuth2Authentication *auth = self.authentication; + NSDictionary *profileDict = [[auth class] dictionaryWithJSONData:data]; + if (profileDict) { + self.userProfile = profileDict; + + // Save the ID into the auth object + NSString *identifier = [profileDict objectForKey:@"id"]; + [auth setUserID:identifier]; + + // Save the email into the auth object + NSString *email = [profileDict objectForKey:@"email"]; + [auth setUserEmail:email]; + + // The verified_email key is a boolean NSNumber in the userinfo + // endpoint response, but it is a string like "true" in the id_token. + // We want to consistently save it as a string of the boolean value, + // like @"1". + id verified = [profileDict objectForKey:@"verified_email"]; + if ([verified isKindOfClass:[NSString class]]) { + verified = [NSNumber numberWithBool:[verified boolValue]]; + } + + [auth setUserEmailIsVerified:[verified stringValue]]; + } +} + +#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.refreshToken != nil + && 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]; + if (encoded != nil) { + 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]; +} + + +// Based on Cyrus Najmabadi's elegent little encoder and decoder from +// http://www.cocoadev.com/index.pl?BaseSixtyFour and on GTLBase64 + ++ (NSData *)decodeWebSafeBase64:(NSString *)base64Str { + static char decodingTable[128]; + static BOOL hasInited = NO; + + if (!hasInited) { + char webSafeEncodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + memset(decodingTable, 0, 128); + for (unsigned int i = 0; i < sizeof(webSafeEncodingTable); i++) { + decodingTable[(unsigned int) webSafeEncodingTable[i]] = (char)i; + } + hasInited = YES; + } + + // The input string should be plain ASCII. + const char *cString = [base64Str cStringUsingEncoding:NSASCIIStringEncoding]; + if (cString == nil) return nil; + + NSInteger inputLength = (NSInteger)strlen(cString); + // Input length is not being restricted to multiples of 4. + if (inputLength == 0) return [NSData data]; + + while (inputLength > 0 && cString[inputLength - 1] == '=') { + inputLength--; + } + + NSInteger outputLength = inputLength * 3 / 4; + NSMutableData* data = [NSMutableData dataWithLength:(NSUInteger)outputLength]; + uint8_t *output = [data mutableBytes]; + + NSInteger inputPoint = 0; + NSInteger outputPoint = 0; + char *table = decodingTable; + + while (inputPoint < inputLength - 1) { + int i0 = cString[inputPoint++]; + int i1 = cString[inputPoint++]; + int i2 = inputPoint < inputLength ? cString[inputPoint++] : 'A'; // 'A' will decode to \0 + int i3 = inputPoint < inputLength ? cString[inputPoint++] : 'A'; + + output[outputPoint++] = (uint8_t)((table[i0] << 2) | (table[i1] >> 4)); + if (outputPoint < outputLength) { + output[outputPoint++] = (uint8_t)(((table[i1] & 0xF) << 4) | (table[i2] >> 2)); + } + if (outputPoint < outputLength) { + output[outputPoint++] = (uint8_t)(((table[i2] & 0x3) << 6) | table[i3]); + } + } + + return data; +} + +#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT + +@end + +#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/example/common/gtm-oauth2/Source/Mac/GTMOAuth2Window.xib b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2Window.xib new file mode 100644 index 00000000..befc2123 --- /dev/null +++ b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2Window.xib @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="4457.6" systemVersion="12E55" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none"> + <dependencies> + <deployment version="1050" defaultVersion="1080" identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="4457.6"/> + <plugIn identifier="com.apple.WebKitIBPlugin" version="3330"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="GTMOAuth2WindowController"> + <connections> + <action selector="closeWindow:" destination="17" id="42"/> + <outlet property="keychainCheckbox" destination="43" id="46"/> + <outlet property="webBackButton" destination="19" id="47"/> + <outlet property="webCloseButton" destination="17" id="48"/> + <outlet property="webView" destination="5" id="49"/> + <outlet property="window" destination="3" id="8"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application"/> + <window title="Sign In" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" wantsToBeColor="NO" visibleAtLaunch="NO" animationBehavior="default" id="3"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES"/> + <windowPositionMask key="initialPositionMask" leftStrut="YES" bottomStrut="YES"/> + <rect key="contentRect" x="74" y="707" width="515" height="419"/> + <rect key="screenRect" x="0.0" y="0.0" width="1920" height="1178"/> + <value key="minSize" type="size" width="475" height="290"/> + <view key="contentView" id="4"> + <rect key="frame" x="0.0" y="0.0" width="515" height="419"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <webView id="5"> + <rect key="frame" x="0.0" y="20" width="515" height="399"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <webPreferences key="preferences" defaultFontSize="12" defaultFixedFontSize="12"/> + <connections> + <action selector="goBack:" destination="19" id="28"/> + <action selector="goForward:" destination="26" id="29"/> + </connections> + </webView> + <button id="17"> + <rect key="frame" x="479" y="0.0" width="16" height="19"/> + <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> + <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSStopProgressTemplate" imagePosition="overlaps" alignment="center" imageScaling="proportionallyDown" inset="2" id="18"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + <string key="keyEquivalent" base64-UTF8="YES"> +Gw +</string> + </buttonCell> + </button> + <button id="19"> + <rect key="frame" x="437" y="0.0" width="16" height="19"/> + <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> + <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSGoLeftTemplate" imagePosition="overlaps" alignment="center" imageScaling="proportionallyDown" inset="2" id="20"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <connections> + <binding destination="-2" name="enabled" keyPath="webView.canGoBack" id="31"/> + </connections> + </button> + <button id="26"> + <rect key="frame" x="456" y="0.0" width="16" height="19"/> + <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> + <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSGoRightTemplate" imagePosition="only" alignment="center" imageScaling="proportionallyDown" inset="2" id="27"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <connections> + <binding destination="-2" name="enabled" keyPath="webView.canGoForward" id="35"/> + </connections> + </button> + <button id="43"> + <rect key="frame" x="2" y="1" width="429" height="18"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <buttonCell key="cell" type="check" title="Stay signed in to this account" bezelStyle="regularSquare" imagePosition="left" alignment="left" controlSize="small" state="on" inset="2" id="44"> + <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> + <font key="font" metaFont="smallSystem"/> + </buttonCell> + </button> + </subviews> + </view> + <connections> + <outlet property="delegate" destination="-2" id="7"/> + </connections> + </window> + <userDefaultsController id="32"/> + </objects> + <resources> + <image name="NSGoLeftTemplate" width="9" height="9"/> + <image name="NSGoRightTemplate" width="9" height="9"/> + <image name="NSStopProgressTemplate" width="11" height="11"/> + </resources> + <classes> + <class className="GTMOAuth2WindowController" superclassName="NSWindowController"> + <source key="sourceIdentifier" type="project" relativePath="./Classes/GTMOAuth2WindowController.h"/> + <relationships> + <relationship kind="action" name="closeWindow:"/> + <relationship kind="outlet" name="completionPlaceholder_"/> + <relationship kind="outlet" name="delegate_"/> + <relationship kind="outlet" name="keychainCheckbox" candidateClass="NSButton"/> + <relationship kind="outlet" name="userData_"/> + <relationship kind="outlet" name="webBackButton" candidateClass="NSButton"/> + <relationship kind="outlet" name="webCloseButton" candidateClass="NSButton"/> + <relationship kind="outlet" name="webView" candidateClass="WebView"/> + </relationships> + </class> + </classes> +</document>
\ No newline at end of file diff --git a/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.h b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.h new file mode 100644 index 00000000..9ff89b70 --- /dev/null +++ b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.h @@ -0,0 +1,332 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// GTMOAuth2WindowController +// +// This window controller for Mac handles sign-in via OAuth2 to Google or +// other services. +// +// This controller is not reusable; create a new instance of this controller +// every time the user will sign in. +// +// Sample usage for signing in to a Google service: +// +// static NSString *const kKeychainItemName = @”My App: Google Plus”; +// NSString *scope = @"https://www.googleapis.com/auth/plus.me"; +// +// +// GTMOAuth2WindowController *windowController; +// windowController = [[[GTMOAuth2WindowController alloc] initWithScope:scope +// clientID:clientID +// clientSecret:clientSecret +// keychainItemName:kKeychainItemName +// resourceBundle:nil] autorelease]; +// +// [windowController signInSheetModalForWindow:mMainWindow +// delegate:self +// finishedSelector:@selector(windowController:finishedWithAuth:error:)]; +// +// The finished selector should have a signature matching this: +// +// - (void)windowController:(GTMOAuth2WindowController *)windowController +// finishedWithAuth:(GTMOAuth2Authentication *)auth +// error:(NSError *)error { +// if (error != nil) { +// // sign in failed +// } else { +// // sign in succeeded +// // +// // with the GTL library, pass the authentication to the service object, +// // like +// // [[self contactService] setAuthorizer:auth]; +// // +// // or use it to sign a request directly, like +// // BOOL isAuthorizing = [self authorizeRequest:request +// // delegate:self +// // didFinishSelector:@selector(auth:finishedWithError:)]; +// } +// } +// +// To sign in to services other than Google, use the longer init method, +// as shown in the sample application +// +// If the network connection is lost for more than 30 seconds while the sign-in +// html is displayed, the notification kGTLOAuthNetworkLost will be sent. + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +#include <Foundation/Foundation.h> + +#if !TARGET_OS_IPHONE + +#import <Cocoa/Cocoa.h> +#import <WebKit/WebKit.h> + +// GTMHTTPFetcher.h brings in GTLDefines/GDataDefines +#import "GTMHTTPFetcher.h" + +#import "GTMOAuth2SignIn.h" +#import "GTMOAuth2Authentication.h" +#import "GTMHTTPFetchHistory.h" // for GTMCookieStorage + +@class GTMOAuth2SignIn; + +@interface GTMOAuth2WindowController : NSWindowController { + @private + // IBOutlets + NSButton *keychainCheckbox_; + WebView *webView_; + NSButton *webCloseButton_; + NSButton *webBackButton_; + + // the object responsible for the sign-in networking sequence; it holds + // onto the authentication object as well + GTMOAuth2SignIn *signIn_; + + // the page request to load when awakeFromNib occurs + NSURLRequest *initialRequest_; + + // local storage for WebKit cookies so they're not shared with Safari + GTMCookieStorage *cookieStorage_; + + // the user we're calling back + // + // the delegate is retained only until the callback is invoked + // or the sign-in is canceled + id delegate_; + SEL finishedSelector_; + +#if NS_BLOCKS_AVAILABLE + void (^completionBlock_)(GTMOAuth2Authentication *, NSError *); +#elif !__LP64__ + // placeholders: for 32-bit builds, keep the size of the object's ivar section + // the same with and without blocks +#ifndef __clang_analyzer__ + id completionPlaceholder_; +#endif +#endif + + // flag allowing application to quit during display of sign-in sheet on 10.6 + // and later + BOOL shouldAllowApplicationTermination_; + + // delegate method for handling URLs to be opened in external windows + SEL externalRequestSelector_; + + BOOL isWindowShown_; + + // paranoid flag to ensure we only close once during the sign-in sequence + BOOL hasDoneFinalRedirect_; + + // paranoid flag to ensure we only call the user back once + BOOL hasCalledFinished_; + + // if non-nil, we display as a sheet on the specified window + NSWindow *sheetModalForWindow_; + + // if non-empty, the name of the application and service used for the + // keychain item + NSString *keychainItemName_; + + // if non-nil, the html string to be displayed immediately upon opening + // of the web view + NSString *initialHTMLString_; + + // if true, we allow default WebView handling of cookies, so the + // same user remains signed in each time the dialog is displayed + BOOL shouldPersistUser_; + + // user-defined data + id userData_; + NSMutableDictionary *properties_; +} + +// User interface elements +@property (nonatomic, assign) IBOutlet NSButton *keychainCheckbox; +@property (nonatomic, assign) IBOutlet WebView *webView; +@property (nonatomic, assign) IBOutlet NSButton *webCloseButton; +@property (nonatomic, assign) IBOutlet NSButton *webBackButton; + +// The application and service name to use for saving the auth tokens +// to the keychain +@property (nonatomic, copy) NSString *keychainItemName; + +// If true, the sign-in will remember which user was last signed in +// +// Defaults to false, so showing the sign-in window will always ask for +// the username and password, rather than skip to the grant authorization +// page. During development, it may be convenient to set this to true +// to speed up signing in. +@property (nonatomic, assign) BOOL shouldPersistUser; + +// Optional html string displayed immediately upon opening the web view +// +// This string is visible just until the sign-in web page loads, and +// may be used for a "Loading..." type of message +@property (nonatomic, copy) NSString *initialHTMLString; + +// The default timeout for an unreachable network during display of the +// sign-in page is 30 seconds, after which the notification +// kGTLOAuthNetworkLost is sent; set this to 0 to have no timeout +@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval; + +// On 10.6 and later, the sheet can allow application termination by calling +// NSWindow's setPreventsApplicationTerminationWhenModal: +@property (nonatomic, assign) BOOL shouldAllowApplicationTermination; + +// Selector for a delegate method to handle requests sent to an external +// browser. +// +// Selector should have a signature matching +// - (void)windowController:(GTMOAuth2WindowController *)controller +// opensRequest:(NSURLRequest *)request; +// +// The controller's default behavior is to use NSWorkspace's openURL: +@property (nonatomic, assign) SEL externalRequestSelector; + +// The underlying object to hold authentication tokens and authorize http +// requests +@property (nonatomic, retain, readonly) GTMOAuth2Authentication *authentication; + +// The underlying object which performs the sign-in networking sequence +@property (nonatomic, retain, readonly) GTMOAuth2SignIn *signIn; + +// Any arbitrary data object the user would like the controller to retain +@property (nonatomic, retain) id userData; + +// Stored property values are retained for the convenience of the caller +- (void)setProperty:(id)obj forKey:(NSString *)key; +- (id)propertyForKey:(NSString *)key; + +@property (nonatomic, retain) NSDictionary *properties; + +- (IBAction)closeWindow:(id)sender; + +// Create a controller for authenticating to Google services +// +// scope is the requested scope of authorization +// (like "http://www.google.com/m8/feeds") +// +// keychainItemName is used for storing the token on the keychain, +// and is required for the "remember for later" checkbox to be shown; +// keychainItemName should be like "My Application: Google Contacts" +// (or set to nil if no persistent keychain storage is desired) +// +// resourceBundle may be nil if the window is in the main bundle's nib +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (id)controllerWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName // may be nil + resourceBundle:(NSBundle *)bundle; // may be nil + +- (id)initWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle; +#endif + +// Create a controller for authenticating to non-Google services, taking +// explicit endpoint URLs and an authentication object ++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName // may be nil + resourceBundle:(NSBundle *)bundle; // may be nil + +// This is the designated initializer +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle; + +// Entry point to begin displaying the sign-in window +// +// the finished selector should have a signature matching +// - (void)windowController:(GTMOAuth2WindowController *)windowController +// finishedWithAuth:(GTMOAuth2Authentication *)auth +// error:(NSError *)error { +// +// Once the finished method has been invoked with no error, the auth object +// may be used to authorize requests (refreshing the access token, if necessary, +// and adding the auth header) like: +// +// [authorizer authorizeRequest:myNSMutableURLRequest] +// delegate:self +// didFinishSelector:@selector(auth:finishedWithError:)]; +// +// or can be stored in a GTL service object like +// GTLServiceGoogleContact *service = [self contactService]; +// [service setAuthorizer:auth]; +// +// The delegate is retained only until the finished selector is invoked or +// the sign-in is canceled +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector; + +#if NS_BLOCKS_AVAILABLE +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + completionHandler:(void (^)(GTMOAuth2Authentication *auth, NSError *error))handler; +#endif + +- (void)cancelSigningIn; + +// Subclasses may override authNibName to specify a custom name ++ (NSString *)authNibName; + +// apps may replace the sign-in class with their own subclass of it ++ (Class)signInClass; ++ (void)setSignInClass:(Class)theClass; + +// Revocation of an authorized token from Google +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth; +#endif + +// Keychain +// +// The keychain checkbox is shown if the keychain application service +// name (typically set in the initWithScope: method) is non-empty +// + +// Create an authentication object for Google services from the access +// token and secret stored in the keychain; if no token is available, return +// an unauthorized auth object +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret; +#endif + +// Add tokens from the keychain, if available, to the authentication object +// +// returns YES if the authentication object was authorized from the keychain ++ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)auth; + +// Method for deleting the stored access token and secret, useful for "signing +// out" ++ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName; + +// Method for saving the stored access token and secret; typically, this method +// is used only by the window controller ++ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)auth; +@end + +#endif // #if !TARGET_OS_IPHONE + +#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.m b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.m new file mode 100644 index 00000000..948975bf --- /dev/null +++ b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.m @@ -0,0 +1,727 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +#if !TARGET_OS_IPHONE + +#import "GTMOAuth2WindowController.h" + +@interface GTMOAuth2WindowController () +@property (nonatomic, retain) GTMOAuth2SignIn *signIn; +@property (nonatomic, copy) NSURLRequest *initialRequest; +@property (nonatomic, retain) GTMCookieStorage *cookieStorage; +@property (nonatomic, retain) NSWindow *sheetModalForWindow; + +- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil; +- (void)setupSheetTerminationHandling; +- (void)destroyWindow; +- (void)handlePrematureWindowClose; +- (BOOL)shouldUseKeychain; +- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request; +- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error; +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo; + +- (void)handleCookiesForResponse:(NSURLResponse *)response; +- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request; +@end + +const char *kKeychainAccountName = "OAuth"; + +@implementation GTMOAuth2WindowController + +// IBOutlets +@synthesize keychainCheckbox = keychainCheckbox_, + webView = webView_, + webCloseButton = webCloseButton_, + webBackButton = webBackButton_; + +// regular ivars +@synthesize signIn = signIn_, + initialRequest = initialRequest_, + cookieStorage = cookieStorage_, + sheetModalForWindow = sheetModalForWindow_, + keychainItemName = keychainItemName_, + initialHTMLString = initialHTMLString_, + shouldAllowApplicationTermination = shouldAllowApplicationTermination_, + externalRequestSelector = externalRequestSelector_, + shouldPersistUser = shouldPersistUser_, + userData = userData_, + properties = properties_; + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +// Create a controller for authenticating to Google services ++ (id)controllerWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + return [[[self alloc] initWithScope:scope + clientID:clientID + clientSecret:clientSecret + keychainItemName:keychainItemName + resourceBundle:bundle] autorelease]; +} + +- (id)initWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + Class signInClass = [[self class] signInClass]; + GTMOAuth2Authentication *auth; + auth = [signInClass standardGoogleAuthenticationForScope:scope + clientID:clientID + clientSecret:clientSecret]; + NSURL *authorizationURL = [signInClass googleAuthorizationURL]; + return [self initWithAuthentication:auth + authorizationURL:authorizationURL + keychainItemName:keychainItemName + resourceBundle:bundle]; +} +#endif + +// Create a controller for authenticating to any service ++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + return [[[self alloc] initWithAuthentication:auth + authorizationURL:authorizationURL + keychainItemName:keychainItemName + resourceBundle:bundle] autorelease]; +} + +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + resourceBundle:(NSBundle *)bundle { + if (bundle == nil) { + bundle = [NSBundle mainBundle]; + } + + NSString *nibName = [[self class] authNibName]; + NSString *nibPath = [bundle pathForResource:nibName + ofType:@"nib"]; + self = [super initWithWindowNibPath:nibPath + owner:self]; + if (self != nil) { + // use the supplied auth and OAuth endpoint URLs + Class signInClass = [[self class] signInClass]; + signIn_ = [[signInClass alloc] initWithAuthentication:auth + authorizationURL:authorizationURL + delegate:self + webRequestSelector:@selector(signIn:displayRequest:) + finishedSelector:@selector(signIn:finishedWithAuth:error:)]; + keychainItemName_ = [keychainItemName copy]; + + // create local, temporary storage for WebKit cookies + cookieStorage_ = [[GTMCookieStorage alloc] init]; + } + return self; +} + +- (void)dealloc { + [signIn_ release]; + [initialRequest_ release]; + [cookieStorage_ release]; + [delegate_ release]; +#if NS_BLOCKS_AVAILABLE + [completionBlock_ release]; +#endif + [sheetModalForWindow_ release]; + [keychainItemName_ release]; + [initialHTMLString_ release]; + [userData_ release]; + [properties_ release]; + + [super dealloc]; +} + +- (void)awakeFromNib { + // load the requested initial sign-in page + [self.webView setResourceLoadDelegate:self]; + [self.webView setPolicyDelegate:self]; + + // the app may prefer some html other than blank white to be displayed + // before the sign-in web page loads + NSString *html = self.initialHTMLString; + if ([html length] > 0) { + [[self.webView mainFrame] loadHTMLString:html baseURL:nil]; + } + + // hide the keychain checkbox if we're not supporting keychain + BOOL hideKeychainCheckbox = ![self shouldUseKeychain]; + + const NSTimeInterval kJanuary2011 = 1293840000; + BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011); + if (isDateValid) { + // start the asynchronous load of the sign-in web page + [[self.webView mainFrame] performSelector:@selector(loadRequest:) + withObject:self.initialRequest + afterDelay:0.01 + inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; + } else { + // clock date is invalid, so signing in would fail with an unhelpful error + // from the server. Warn the user in an html string showing a watch icon, + // question mark, and the system date and time. Hopefully this will clue + // in brighter users, or at least let them make a useful screenshot to show + // to developers. + // + // Even better is for apps to check the system clock and show some more + // helpful, localized instructions for users; this is really a fallback. + NSString *const htmlTemplate = @"<html><body><div align=center><font size='7'>" + @"⌚ ?<br><i>System Clock Incorrect</i><br>%@" + @"</font></div></body></html>"; + NSString *errHTML = [NSString stringWithFormat:htmlTemplate, [NSDate date]]; + + [[webView_ mainFrame] loadHTMLString:errHTML baseURL:nil]; + hideKeychainCheckbox = YES; + } + +#if DEBUG + // Verify that Javascript is enabled + BOOL hasJS = [[webView_ preferences] isJavaScriptEnabled]; + NSAssert(hasJS, @"GTMOAuth2: Javascript is required"); +#endif + + [keychainCheckbox_ setHidden:hideKeychainCheckbox]; +} + ++ (NSString *)authNibName { + // subclasses may override this to specify a custom nib name + return @"GTMOAuth2Window"; +} + +#pragma mark - + +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector { + // check the selector on debug builds + GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector, + @encode(GTMOAuth2WindowController *), @encode(GTMOAuth2Authentication *), + @encode(NSError *), 0); + + delegate_ = [delegate retain]; + finishedSelector_ = finishedSelector; + + [self signInCommonForWindow:parentWindowOrNil]; +} + +#if NS_BLOCKS_AVAILABLE +- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil + completionHandler:(void (^)(GTMOAuth2Authentication *, NSError *))handler { + completionBlock_ = [handler copy]; + + [self signInCommonForWindow:parentWindowOrNil]; +} +#endif + +- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil { + self.sheetModalForWindow = parentWindowOrNil; + hasDoneFinalRedirect_ = NO; + hasCalledFinished_ = NO; + + [self.signIn startSigningIn]; +} + +- (void)cancelSigningIn { + // The user has explicitly asked us to cancel signing in + // (so no further callback is required) + hasCalledFinished_ = YES; + + [delegate_ autorelease]; + delegate_ = nil; + +#if NS_BLOCKS_AVAILABLE + [completionBlock_ autorelease]; + completionBlock_ = nil; +#endif + + // The signIn object's cancel method will close the window + [self.signIn cancelSigningIn]; + hasDoneFinalRedirect_ = YES; +} + +- (IBAction)closeWindow:(id)sender { + // dismiss the window/sheet before we call back the client + [self destroyWindow]; + [self handlePrematureWindowClose]; +} + +#pragma mark SignIn callbacks + +- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request { + // this is the signIn object's webRequest method, telling the controller + // to either display the request in the webview, or close the window + // + // All web requests and all window closing goes through this routine + +#if DEBUG + if ((isWindowShown_ && request != nil) + || (!isWindowShown_ && request == nil)) { + NSLog(@"Window state unexpected for request %@", [request URL]); + return; + } +#endif + + if (request != nil) { + // display the request + self.initialRequest = request; + + NSWindow *parentWindow = self.sheetModalForWindow; + if (parentWindow) { + [self setupSheetTerminationHandling]; + + NSWindow *sheet = [self window]; + [NSApp beginSheet:sheet + modalForWindow:parentWindow + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:nil]; + } else { + // modeless + [self showWindow:self]; + } + isWindowShown_ = YES; + } else { + // request was nil + [self destroyWindow]; + } +} + +- (void)setupSheetTerminationHandling { + NSWindow *sheet = [self window]; + + SEL sel = @selector(setPreventsApplicationTerminationWhenModal:); + if ([sheet respondsToSelector:sel]) { + // setPreventsApplicationTerminationWhenModal is available in NSWindow + // on 10.6 and later + BOOL boolVal = !self.shouldAllowApplicationTermination; + NSMethodSignature *sig = [sheet methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:sheet]; + [invocation setArgument:&boolVal atIndex:2]; + [invocation invoke]; + } +} + +- (void)destroyWindow { + // no request; close the window + + // Avoid more callbacks after the close happens, as the window + // controller may be gone. + [self.webView stopLoading:nil]; + + NSWindow *parentWindow = self.sheetModalForWindow; + if (parentWindow) { + [NSApp endSheet:[self window]]; + } else { + // defer closing the window, in case we're responding to some window event + [[self window] performSelector:@selector(close) + withObject:nil + afterDelay:0.1 + inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; + + } + isWindowShown_ = NO; +} + +- (void)handlePrematureWindowClose { + if (!hasDoneFinalRedirect_) { + // tell the sign-in object to tell the user's finished method + // that we're done + [self.signIn windowWasClosed]; + hasDoneFinalRedirect_ = YES; + } +} + +- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { + [sheet orderOut:self]; + + self.sheetModalForWindow = nil; +} + +- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error { + if (!hasCalledFinished_) { + hasCalledFinished_ = YES; + + if (error == nil) { + BOOL shouldUseKeychain = [self shouldUseKeychain]; + if (shouldUseKeychain) { + BOOL canAuthorize = auth.canAuthorize; + BOOL isKeychainChecked = ([keychainCheckbox_ state] == NSOnState); + + NSString *keychainItemName = self.keychainItemName; + + if (isKeychainChecked && canAuthorize) { + // save the auth params in the keychain + [[self class] saveAuthToKeychainForName:keychainItemName + authentication:auth]; + } else { + // remove the auth params from the keychain + [[self class] removeAuthFromKeychainForName:keychainItemName]; + } + } + } + + if (delegate_ && finishedSelector_) { + SEL sel = finishedSelector_; + NSMethodSignature *sig = [delegate_ methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:delegate_]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&auth atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + } + + [delegate_ autorelease]; + delegate_ = nil; + +#if NS_BLOCKS_AVAILABLE + if (completionBlock_) { + completionBlock_(auth, error); + + // release the block here to avoid a retain loop on the controller + [completionBlock_ autorelease]; + completionBlock_ = nil; + } +#endif + } +} + +static Class gSignInClass = Nil; + ++ (Class)signInClass { + if (gSignInClass == Nil) { + gSignInClass = [GTMOAuth2SignIn class]; + } + return gSignInClass; +} + ++ (void)setSignInClass:(Class)theClass { + gSignInClass = theClass; +} + +#pragma mark Token Revocation + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth { + [[self signInClass] revokeTokenForGoogleAuthentication:auth]; +} +#endif + +#pragma mark WebView methods + +- (NSURLRequest *)webView:(WebView *)sender resource:(id)identifier willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse fromDataSource:(WebDataSource *)dataSource { + // override WebKit's cookie storage with our own to avoid cookie persistence + // across sign-ins and interaction with the Safari browser's sign-in state + [self handleCookiesForResponse:redirectResponse]; + request = [self addCookiesToRequest:request]; + + if (!hasDoneFinalRedirect_) { + hasDoneFinalRedirect_ = [self.signIn requestRedirectedToRequest:request]; + if (hasDoneFinalRedirect_) { + // signIn has told the window to close + return nil; + } + } + return request; +} + +- (void)webView:(WebView *)sender resource:(id)identifier didReceiveResponse:(NSURLResponse *)response fromDataSource:(WebDataSource *)dataSource { + // override WebKit's cookie storage with our own + [self handleCookiesForResponse:response]; +} + +- (void)webView:(WebView *)sender resource:(id)identifier didFinishLoadingFromDataSource:(WebDataSource *)dataSource { + NSString *title = [sender stringByEvaluatingJavaScriptFromString:@"document.title"]; + if ([title length] > 0) { + [self.signIn titleChanged:title]; + } + + [signIn_ cookiesChanged:(NSHTTPCookieStorage *)cookieStorage_]; +} + +- (void)webView:(WebView *)sender resource:(id)identifier didFailLoadingWithError:(NSError *)error fromDataSource:(WebDataSource *)dataSource { + [self.signIn loadFailedWithError:error]; +} + +- (void)windowWillClose:(NSNotification *)note { + if (isWindowShown_) { + [self handlePrematureWindowClose]; + } + isWindowShown_ = NO; +} + +- (void)webView:(WebView *)webView +decidePolicyForNewWindowAction:(NSDictionary *)actionInformation + request:(NSURLRequest *)request + newFrameName:(NSString *)frameName +decisionListener:(id<WebPolicyDecisionListener>)listener { + SEL sel = self.externalRequestSelector; + if (sel) { + [delegate_ performSelector:sel + withObject:self + withObject:request]; + } else { + // default behavior is to open the URL in NSWorkspace's default browser + NSURL *url = [request URL]; + [[NSWorkspace sharedWorkspace] openURL:url]; + } + [listener ignore]; +} + +#pragma mark Cookie management + +// Rather than let the WebView use Safari's default cookie storage, we intercept +// requests and response to segregate and later discard cookies from signing in. +// +// This allows the application to actually sign out by discarding the auth token +// rather than the user being kept signed in by the cookies. + +- (void)handleCookiesForResponse:(NSURLResponse *)response { + if (self.shouldPersistUser) { + // we'll let WebKit handle the cookies; they'll persist across apps + // and across runs of this app + return; + } + + if ([response respondsToSelector:@selector(allHeaderFields)]) { + // grab the cookies from the header as NSHTTPCookies and store them locally + NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields]; + if (headers) { + NSURL *url = [response URL]; + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers + forURL:url]; + if ([cookies count] > 0) { + [cookieStorage_ setCookies:cookies]; + } + } + } +} + +- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request { + if (self.shouldPersistUser) { + // we'll let WebKit handle the cookies; they'll persist across apps + // and across runs of this app + return request; + } + + // override WebKit's usual automatic storage of cookies + NSMutableURLRequest *mutableRequest = [[request mutableCopy] autorelease]; + [mutableRequest setHTTPShouldHandleCookies:NO]; + + // add our locally-stored cookies for this URL, if any + NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]]; + if ([cookies count] > 0) { + NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + NSString *cookieHeader = [headers objectForKey:@"Cookie"]; + if (cookieHeader) { + [mutableRequest setValue:cookieHeader forHTTPHeaderField:@"Cookie"]; + } + } + return mutableRequest; +} + +#pragma mark Keychain support + ++ (NSString *)prefsKeyForName:(NSString *)keychainItemName { + NSString *result = [@"OAuth2: " stringByAppendingString:keychainItemName]; + return result; +} + ++ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)auth { + + [self removeAuthFromKeychainForName:keychainItemName]; + + // don't save unless we have a token that can really authorize requests + if (!auth.canAuthorize) return NO; + + // make a response string containing the values we want to save + NSString *password = [auth persistenceResponseString]; + + SecKeychainRef defaultKeychain = NULL; + SecKeychainItemRef *dontWantItemRef= NULL; + const char *utf8ServiceName = [keychainItemName UTF8String]; + const char *utf8Password = [password UTF8String]; + + OSStatus err = SecKeychainAddGenericPassword(defaultKeychain, + (UInt32) strlen(utf8ServiceName), utf8ServiceName, + (UInt32) strlen(kKeychainAccountName), kKeychainAccountName, + (UInt32) strlen(utf8Password), utf8Password, + dontWantItemRef); + BOOL didSucceed = (err == noErr); + if (didSucceed) { + // write to preferences that we have a keychain item (so we know later + // that we can read from the keychain without raising a permissions dialog) + NSString *prefKey = [self prefsKeyForName:keychainItemName]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:YES forKey:prefKey]; + } + + return didSucceed; +} + ++ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName { + + SecKeychainRef defaultKeychain = NULL; + SecKeychainItemRef itemRef = NULL; + const char *utf8ServiceName = [keychainItemName UTF8String]; + + // we don't really care about the password here, we just want to + // get the SecKeychainItemRef so we can delete it. + OSStatus err = SecKeychainFindGenericPassword (defaultKeychain, + (UInt32) strlen(utf8ServiceName), utf8ServiceName, + (UInt32) strlen(kKeychainAccountName), kKeychainAccountName, + 0, NULL, // ignore password + &itemRef); + if (err != noErr) { + // failure to find is success + return YES; + } else { + // found something, so delete it + err = SecKeychainItemDelete(itemRef); + CFRelease(itemRef); + + // remove our preference key + NSString *prefKey = [self prefsKeyForName:keychainItemName]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults removeObjectForKey:prefKey]; + + return (err == noErr); + } +} + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret { + Class signInClass = [self signInClass]; + NSURL *tokenURL = [signInClass googleTokenURL]; + NSString *redirectURI = [signInClass nativeClientRedirectURI]; + + GTMOAuth2Authentication *auth; + auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle + tokenURL:tokenURL + redirectURI:redirectURI + clientID:clientID + clientSecret:clientSecret]; + + [GTMOAuth2WindowController authorizeFromKeychainForName:keychainItemName + authentication:auth]; + return auth; +} +#endif + ++ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)newAuth { + [newAuth setAccessToken:nil]; + + // before accessing the keychain, check preferences to verify that we've + // previously saved a token to the keychain (so we don't needlessly raise + // a keychain access permission dialog) + NSString *prefKey = [self prefsKeyForName:keychainItemName]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + BOOL flag = [defaults boolForKey:prefKey]; + if (!flag) { + return NO; + } + + BOOL didGetTokens = NO; + + SecKeychainRef defaultKeychain = NULL; + const char *utf8ServiceName = [keychainItemName UTF8String]; + SecKeychainItemRef *dontWantItemRef = NULL; + + void *passwordBuff = NULL; + UInt32 passwordBuffLength = 0; + + OSStatus err = SecKeychainFindGenericPassword(defaultKeychain, + (UInt32) strlen(utf8ServiceName), utf8ServiceName, + (UInt32) strlen(kKeychainAccountName), kKeychainAccountName, + &passwordBuffLength, &passwordBuff, + dontWantItemRef); + if (err == noErr && passwordBuff != NULL) { + + NSString *password = [[[NSString alloc] initWithBytes:passwordBuff + length:passwordBuffLength + encoding:NSUTF8StringEncoding] autorelease]; + + // free the password buffer that was allocated above + SecKeychainItemFreeContent(NULL, passwordBuff); + + if (password != nil) { + [newAuth setKeysForResponseString:password]; + didGetTokens = YES; + } + } + return didGetTokens; +} + +#pragma mark User Properties + +- (void)setProperty:(id)obj forKey:(NSString *)key { + if (obj == nil) { + // User passed in nil, so delete the property + [properties_ removeObjectForKey:key]; + } else { + // Be sure the property dictionary exists + if (properties_ == nil) { + [self setProperties:[NSMutableDictionary dictionary]]; + } + [properties_ setObject:obj forKey:key]; + } +} + +- (id)propertyForKey:(NSString *)key { + id obj = [properties_ objectForKey:key]; + + // Be sure the returned pointer has the life of the autorelease pool, + // in case self is released immediately + return [[obj retain] autorelease]; +} + +#pragma mark Accessors + +- (GTMOAuth2Authentication *)authentication { + return self.signIn.authentication; +} + +- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val { + self.signIn.networkLossTimeoutInterval = val; +} + +- (NSTimeInterval)networkLossTimeoutInterval { + return self.signIn.networkLossTimeoutInterval; +} + +- (BOOL)shouldUseKeychain { + NSString *name = self.keychainItemName; + return ([name length] > 0); +} + +@end + +#endif // #if !TARGET_OS_IPHONE + +#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.h b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.h new file mode 100644 index 00000000..33adbf71 --- /dev/null +++ b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.h @@ -0,0 +1,376 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// GTMOAuth2ViewControllerTouch.h +// +// This view controller for iPhone handles sign-in via OAuth to Google or +// other services. +// +// This controller is not reusable; create a new instance of this controller +// every time the user will sign in. +// + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +#import <Foundation/Foundation.h> + +#if TARGET_OS_IPHONE + +#import <UIKit/UIKit.h> + +#import "GTMOAuth2Authentication.h" + +#undef _EXTERN +#undef _INITIALIZE_AS +#ifdef GTMOAUTH2VIEWCONTROLLERTOUCH_DEFINE_GLOBALS +#define _EXTERN +#define _INITIALIZE_AS(x) =x +#else +#define _EXTERN extern +#define _INITIALIZE_AS(x) +#endif + +_EXTERN NSString* const kGTMOAuth2KeychainErrorDomain _INITIALIZE_AS(@"com.google.GTMOAuthKeychain"); + +@class GTMOAuth2SignIn; +@class GTMOAuth2ViewControllerTouch; + +typedef void (^GTMOAuth2ViewControllerCompletionHandler)(GTMOAuth2ViewControllerTouch *viewController, GTMOAuth2Authentication *auth, NSError *error); + +@interface GTMOAuth2ViewControllerTouch : UIViewController<UINavigationControllerDelegate, UIWebViewDelegate> { + @private + UIButton *backButton_; + UIButton *forwardButton_; + UIActivityIndicatorView *initialActivityIndicator_; + UIView *navButtonsView_; + UIBarButtonItem *rightBarButtonItem_; + UIWebView *webView_; + + // 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 *request_; + + // 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 + GTMOAuth2ViewControllerCompletionHandler completionBlock_; + + void (^popViewBlock_)(void); +#endif + + NSString *keychainItemName_; + CFTypeRef keychainItemAccessibility_; + + // if non-nil, the html string to be displayed immediately upon opening + // of the web view + NSString *initialHTMLString_; + + // set to 1 or -1 if the user sets the showsInitialActivityIndicator + // property + int mustShowActivityIndicator_; + + // if non-nil, the URL for which cookies will be deleted when the + // browser view is dismissed + NSURL *browserCookiesURL_; + + id userData_; + NSMutableDictionary *properties_; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000 + // We delegate the decision to our owning NavigationController (if any). + // But, the NavigationController will call us back, and ask us. + // BOOL keeps us from infinite looping. + BOOL isInsideShouldAutorotateToInterfaceOrientation_; +#endif + + // YES, when view first shown in this signIn session. + BOOL isViewShown_; + + // YES, after the view has fully transitioned in. + BOOL didViewAppear_; + + // YES between sends of start and stop notifications + BOOL hasNotifiedWebViewStartedLoading_; + + // To prevent us from calling our delegate's selector more than once. + BOOL hasCalledFinished_; + + // Set in a webView callback. + BOOL hasDoneFinalRedirect_; + + // Set during the pop initiated by the sign-in object; otherwise, + // viewWillDisappear indicates that some external change of the view + // has stopped the sign-in. + BOOL didDismissSelf_; +} + +// the application and service name to use for saving the auth tokens +// to the keychain +@property (nonatomic, copy) NSString *keychainItemName; + +// the keychain item accessibility is a system constant for use +// with kSecAttrAccessible. +// +// Since it's a system constant, we do not need to retain it. +@property (nonatomic, assign) CFTypeRef keychainItemAccessibility; + +// 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 or to set the +// initial view color +@property (nonatomic, copy) NSString *initialHTMLString; + +// an activity indicator shows during initial webview load when no initial HTML +// string is specified, but the activity indicator can be forced to be shown +// with this property +@property (nonatomic, assign) BOOL showsInitialActivityIndicator; + +// 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; + +// user interface elements +@property (nonatomic, retain) IBOutlet UIButton *backButton; +@property (nonatomic, retain) IBOutlet UIButton *forwardButton; +@property (nonatomic, retain) IBOutlet UIActivityIndicatorView *initialActivityIndicator; +@property (nonatomic, retain) IBOutlet UIView *navButtonsView; +@property (nonatomic, retain) IBOutlet UIBarButtonItem *rightBarButtonItem; +@property (nonatomic, retain) IBOutlet UIWebView *webView; + +#if NS_BLOCKS_AVAILABLE +// An optional block to be called when the view should be popped. If not set, +// the view controller will use its navigation controller to pop the view. +@property (nonatomic, copy) void (^popViewBlock)(void); +#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; + +// if set, cookies are deleted for this URL when the view is hidden +// +// For Google sign-ins, this is set by default to https://google.com/accounts +// but it may be explicitly set to nil to disable clearing of browser cookies +@property (nonatomic, retain) NSURL *browserCookiesURL; + +// userData is retained for the convenience of the caller +@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; + +// Method for creating a controller to authenticate to Google services +// +// scope is the requested scope of authorization +// (like "http://www.google.com/m8/feeds") +// +// keychain item name is used for storing the token on the keychain, +// keychainItemName should be like "My Application: Google Latitude" +// (or set to nil if no persistent keychain storage is desired) +// +// the delegate is retained only until the finished selector is invoked +// or the sign-in is canceled +// +// If you don't like the default nibName and bundle, you can change them +// using the UIViewController properties once you've made one of these. +// +// finishedSelector is called after authentication completes. It should follow +// this signature. +// +// - (void)viewController:(GTMOAuth2ViewControllerTouch *)viewController +// finishedWithAuth:(GTMOAuth2Authentication *)auth +// error:(NSError *)error; +// +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (id)controllerWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector; + +- (id)initWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector; + +#if NS_BLOCKS_AVAILABLE ++ (id)controllerWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler; + +- (id)initWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler; +#endif +#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 + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector; + +// This is the designated initializer +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector; + +#if NS_BLOCKS_AVAILABLE ++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName // may be nil + completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler; + +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler; +#endif + +// subclasses may override authNibName to specify a custom name ++ (NSString *)authNibName; + +// subclasses may override authNibBundle to specify a custom bundle ++ (NSBundle *)authNibBundle; + +// subclasses may override setUpNavigation to provide their own navigation +// controls +- (void)setUpNavigation; + +// apps may replace the sign-in class with their own subclass of it ++ (Class)signInClass; ++ (void)setSignInClass:(Class)theClass; + +- (void)cancelSigningIn; + +// revocation of an authorized token from Google +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth; +#endif + +// +// Keychain +// + +// 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. OK to pass NULL for the error parameter. +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + error:(NSError **)error; +// Equivalent to calling the method above with a NULL error parameter. ++ (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 + error:(NSError **)error; + +// 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 +// +// returns YES if the save was successful. OK to pass NULL for the error +// parameter. ++ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName + accessibility:(CFTypeRef)accessibility + authentication:(GTMOAuth2Authentication *)auth + error:(NSError **)error; + +// older version, defaults to kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ++ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)auth; + +@end + +// To function, GTMOAuth2ViewControllerTouch needs a certain amount of access +// to the iPhone's keychain. To keep things simple, its keychain access is +// broken out into a helper class. We declare it here in case you'd like to use +// it too, to store passwords. + +enum { + kGTMOAuth2KeychainErrorBadArguments = -1301, + kGTMOAuth2KeychainErrorNoPassword = -1302 +}; + + +@interface GTMOAuth2Keychain : NSObject + ++ (GTMOAuth2Keychain *)defaultKeychain; + +// OK to pass nil for the error parameter. +- (NSString *)passwordForService:(NSString *)service + account:(NSString *)account + error:(NSError **)error; + +// OK to pass nil for the error parameter. +- (BOOL)removePasswordForService:(NSString *)service + account:(NSString *)account + error:(NSError **)error; + +// OK to pass nil for the error parameter. +// +// accessibility should be one of the constants for kSecAttrAccessible +// such as kSecAttrAccessibleWhenUnlocked +- (BOOL)setPassword:(NSString *)password + forService:(NSString *)service + accessibility:(CFTypeRef)accessibility + account:(NSString *)account + error:(NSError **)error; + +// For unit tests: allow setting a mock object ++ (void)setDefaultKeychain:(GTMOAuth2Keychain *)keychain; + +@end + +#endif // TARGET_OS_IPHONE + +#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.m b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.m new file mode 100644 index 00000000..3a18104d --- /dev/null +++ b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.m @@ -0,0 +1,1070 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// GTMOAuth2ViewControllerTouch.m +// + +#import <Foundation/Foundation.h> +#import <Security/Security.h> + +#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES + +#if TARGET_OS_IPHONE + +#define GTMOAUTH2VIEWCONTROLLERTOUCH_DEFINE_GLOBALS 1 +#import "GTMOAuth2ViewControllerTouch.h" + +#import "GTMOAuth2SignIn.h" +#import "GTMOAuth2Authentication.h" + +static NSString * const kGTMOAuth2AccountName = @"OAuth"; +static GTMOAuth2Keychain* sDefaultKeychain = nil; + +@interface GTMOAuth2ViewControllerTouch() + +@property (nonatomic, copy) NSURLRequest *request; + +- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request; +- (void)signIn:(GTMOAuth2SignIn *)signIn +finishedWithAuth:(GTMOAuth2Authentication *)auth + error:(NSError *)error; +- (BOOL)isNavigationBarTranslucent; +- (void)moveWebViewFromUnderNavigationBar; +- (void)popView; +- (void)clearBrowserCookies; +@end + +@implementation GTMOAuth2ViewControllerTouch + +// IBOutlets +@synthesize request = request_, + backButton = backButton_, + forwardButton = forwardButton_, + navButtonsView = navButtonsView_, + rightBarButtonItem = rightBarButtonItem_, + webView = webView_, + initialActivityIndicator = initialActivityIndicator_; + +@synthesize keychainItemName = keychainItemName_, + keychainItemAccessibility = keychainItemAccessibility_, + initialHTMLString = initialHTMLString_, + browserCookiesURL = browserCookiesURL_, + signIn = signIn_, + userData = userData_, + properties = properties_; + +#if NS_BLOCKS_AVAILABLE +@synthesize popViewBlock = popViewBlock_; +#endif + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (id)controllerWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector { + return [[[self alloc] initWithScope:scope + clientID:clientID + clientSecret:clientSecret + keychainItemName:keychainItemName + delegate:delegate + finishedSelector:finishedSelector] autorelease]; +} + +- (id)initWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector { + // convenient entry point for Google authentication + + Class signInClass = [[self class] signInClass]; + + GTMOAuth2Authentication *auth; + auth = [signInClass standardGoogleAuthenticationForScope:scope + clientID:clientID + clientSecret:clientSecret]; + NSURL *authorizationURL = [signInClass googleAuthorizationURL]; + return [self initWithAuthentication:auth + authorizationURL:authorizationURL + keychainItemName:keychainItemName + delegate:delegate + finishedSelector:finishedSelector]; +} + +#if NS_BLOCKS_AVAILABLE + ++ (id)controllerWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler { + return [[[self alloc] initWithScope:scope + clientID:clientID + clientSecret:clientSecret + keychainItemName:keychainItemName + completionHandler:handler] autorelease]; +} + +- (id)initWithScope:(NSString *)scope + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + keychainItemName:(NSString *)keychainItemName + completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler { + // convenient entry point for Google authentication + + Class signInClass = [[self class] signInClass]; + + GTMOAuth2Authentication *auth; + auth = [signInClass standardGoogleAuthenticationForScope:scope + clientID:clientID + clientSecret:clientSecret]; + NSURL *authorizationURL = [signInClass googleAuthorizationURL]; + self = [self initWithAuthentication:auth + authorizationURL:authorizationURL + keychainItemName:keychainItemName + delegate:nil + finishedSelector:NULL]; + if (self) { + completionBlock_ = [handler copy]; + } + return self; +} + +#endif // NS_BLOCKS_AVAILABLE +#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT + ++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector { + return [[[self alloc] initWithAuthentication:auth + authorizationURL:authorizationURL + keychainItemName:keychainItemName + delegate:delegate + finishedSelector:finishedSelector] autorelease]; +} + +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + delegate:(id)delegate + finishedSelector:(SEL)finishedSelector { + + NSString *nibName = [[self class] authNibName]; + NSBundle *nibBundle = [[self class] authNibBundle]; + + self = [super initWithNibName:nibName bundle:nibBundle]; + if (self != nil) { + delegate_ = [delegate retain]; + finishedSelector_ = finishedSelector; + + Class signInClass = [[self class] signInClass]; + + // use the supplied auth and OAuth endpoint URLs + signIn_ = [[signInClass alloc] initWithAuthentication:auth + authorizationURL:authorizationURL + delegate:self + webRequestSelector:@selector(signIn:displayRequest:) + finishedSelector:@selector(signIn:finishedWithAuth:error:)]; + + // if the user is signing in to a Google service, we'll delete the + // Google authentication browser cookies upon completion + // + // for other service domains, or to disable clearing of the cookies, + // set the browserCookiesURL property explicitly + NSString *authorizationHost = [signIn_.authorizationURL host]; + if ([authorizationHost hasSuffix:@".google.com"]) { + NSString *urlStr = [NSString stringWithFormat:@"https://%@/", + authorizationHost]; + NSURL *cookiesURL = [NSURL URLWithString:urlStr]; + [self setBrowserCookiesURL:cookiesURL]; + } + + [self setKeychainItemName:keychainItemName]; + } + return self; +} + +#if NS_BLOCKS_AVAILABLE ++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler { + return [[[self alloc] initWithAuthentication:auth + authorizationURL:authorizationURL + keychainItemName:keychainItemName + completionHandler:handler] autorelease]; +} + +- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth + authorizationURL:(NSURL *)authorizationURL + keychainItemName:(NSString *)keychainItemName + completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler { + // fall back to the non-blocks init + self = [self initWithAuthentication:auth + authorizationURL:authorizationURL + keychainItemName:keychainItemName + delegate:nil + finishedSelector:NULL]; + if (self) { + completionBlock_ = [handler copy]; + } + return self; +} +#endif + +- (void)dealloc { + [webView_ setDelegate:nil]; + + [backButton_ release]; + [forwardButton_ release]; + [initialActivityIndicator_ release]; + [navButtonsView_ release]; + [rightBarButtonItem_ release]; + [webView_ release]; + [signIn_ release]; + [request_ release]; + [delegate_ release]; +#if NS_BLOCKS_AVAILABLE + [completionBlock_ release]; + [popViewBlock_ release]; +#endif + [keychainItemName_ release]; + [initialHTMLString_ release]; + [browserCookiesURL_ release]; + [userData_ release]; + [properties_ release]; + + [super dealloc]; +} + ++ (NSString *)authNibName { + // subclasses may override this to specify a custom nib name + return @"GTMOAuth2ViewTouch"; +} + ++ (NSBundle *)authNibBundle { + // subclasses may override this to specify a custom nib bundle + return nil; +} + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret { + return [self authForGoogleFromKeychainForName:keychainItemName + clientID:clientID + clientSecret:clientSecret + error:NULL]; +} + ++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName + clientID:(NSString *)clientID + clientSecret:(NSString *)clientSecret + error:(NSError **)error { + Class signInClass = [self signInClass]; + NSURL *tokenURL = [signInClass googleTokenURL]; + NSString *redirectURI = [signInClass nativeClientRedirectURI]; + + GTMOAuth2Authentication *auth; + auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle + tokenURL:tokenURL + redirectURI:redirectURI + clientID:clientID + clientSecret:clientSecret]; + [[self class] authorizeFromKeychainForName:keychainItemName + authentication:auth + error:error]; + return auth; +} + +#endif + ++ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)newAuth + error:(NSError **)error { + newAuth.accessToken = nil; + + BOOL didGetTokens = NO; + GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain]; + NSString *password = [keychain passwordForService:keychainItemName + account:kGTMOAuth2AccountName + error:error]; + if (password != nil) { + [newAuth setKeysForResponseString:password]; + didGetTokens = YES; + } + return didGetTokens; +} + ++ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName { + GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain]; + return [keychain removePasswordForService:keychainItemName + account:kGTMOAuth2AccountName + error:nil]; +} + ++ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName + authentication:(GTMOAuth2Authentication *)auth { + return [self saveParamsToKeychainForName:keychainItemName + accessibility:NULL + authentication:auth + error:NULL]; +} + ++ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName + accessibility:(CFTypeRef)accessibility + authentication:(GTMOAuth2Authentication *)auth + error:(NSError **)error { + [self removeAuthFromKeychainForName:keychainItemName]; + // don't save unless we have a token that can really authorize requests + if (![auth canAuthorize]) { + if (error) { + *error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain + code:kGTMOAuth2ErrorTokenUnavailable + userInfo:nil]; + } + return NO; + } + + if (accessibility == NULL + && &kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly != NULL) { + accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly; + } + + // make a response string containing the values we want to save + NSString *password = [auth persistenceResponseString]; + GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain]; + return [keychain setPassword:password + forService:keychainItemName + accessibility:accessibility + account:kGTMOAuth2AccountName + error:error]; +} + +- (void)loadView { + NSString *nibPath = nil; + NSBundle *nibBundle = [self nibBundle]; + if (nibBundle == nil) { + nibBundle = [NSBundle mainBundle]; + } + NSString *nibName = self.nibName; + if (nibName != nil) { + nibPath = [nibBundle pathForResource:nibName ofType:@"nib"]; + } + if (nibPath != nil && [[NSFileManager defaultManager] fileExistsAtPath:nibPath]) { + [super loadView]; + } else { + // One of the requirements of loadView is that a valid view object is set to + // self.view upon completion. Otherwise, subclasses that attempt to + // access self.view after calling [super loadView] will enter an infinite + // loop due to the fact that UIViewController's -view accessor calls + // loadView when self.view is nil. + self.view = [[[UIView alloc] init] autorelease]; + +#if DEBUG + NSLog(@"missing %@.nib", nibName); +#endif + } +} + + +- (void)viewDidLoad { + [self setUpNavigation]; +} + +- (void)setUpNavigation { + rightBarButtonItem_.customView = navButtonsView_; + self.navigationItem.rightBarButtonItem = rightBarButtonItem_; +} + +- (void)popView { +#if NS_BLOCKS_AVAILABLE + void (^popViewBlock)() = self.popViewBlock; +#else + id popViewBlock = nil; +#endif + + if (popViewBlock || self.navigationController.topViewController == self) { + if (!self.view.hidden) { + // Set the flag to our viewWillDisappear method so it knows + // this is a disappearance initiated by the sign-in object, + // not the user cancelling via the navigation controller + didDismissSelf_ = YES; + + if (popViewBlock) { +#if NS_BLOCKS_AVAILABLE + popViewBlock(); + self.popViewBlock = nil; +#endif + } else { + [self.navigationController popViewControllerAnimated:YES]; + } + self.view.hidden = YES; + } + } +} + +- (void)notifyWithName:(NSString *)name + webView:(UIWebView *)webView + kind:(NSString *)kind { + BOOL isStarting = [name isEqual:kGTMOAuth2WebViewStartedLoading]; + if (hasNotifiedWebViewStartedLoading_ == isStarting) { + // Duplicate notification + // + // UIWebView's delegate methods are so unbalanced that there's little + // point trying to keep a count, as it could easily end up stuck greater + // than zero. + // + // We don't really have a way to track the starts and stops of + // subframe loads, too, as the webView in the notification is always + // for the topmost request. + return; + } + hasNotifiedWebViewStartedLoading_ = isStarting; + + // Notification for webview load starting and stopping + NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: + webView, kGTMOAuth2WebViewKey, + kind, kGTMOAuth2WebViewStopKindKey, // kind may be nil + nil]; + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName:name + object:self + userInfo:dict]; +} + +- (void)cancelSigningIn { + // The application has explicitly asked us to cancel signing in + // (so no further callback is required) + hasCalledFinished_ = YES; + + [delegate_ autorelease]; + delegate_ = nil; + +#if NS_BLOCKS_AVAILABLE + [completionBlock_ autorelease]; + completionBlock_ = nil; +#endif + + // The sign-in object's cancel method will close the window + [signIn_ cancelSigningIn]; + hasDoneFinalRedirect_ = YES; +} + +static Class gSignInClass = Nil; + ++ (Class)signInClass { + if (gSignInClass == Nil) { + gSignInClass = [GTMOAuth2SignIn class]; + } + return gSignInClass; +} + ++ (void)setSignInClass:(Class)theClass { + gSignInClass = theClass; +} + +#pragma mark Token Revocation + +#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT ++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth { + [[self signInClass] revokeTokenForGoogleAuthentication:auth]; +} +#endif + +#pragma mark Browser Cookies + +- (GTMOAuth2Authentication *)authentication { + return self.signIn.authentication; +} + +- (void)clearBrowserCookies { + // if browserCookiesURL is non-nil, then get cookies for that URL + // and delete them from the common application cookie storage + NSURL *cookiesURL = [self browserCookiesURL]; + if (cookiesURL) { + NSHTTPCookieStorage *cookieStorage; + + cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + NSArray *cookies = [cookieStorage cookiesForURL:cookiesURL]; + + for (NSHTTPCookie *cookie in cookies) { + [cookieStorage deleteCookie:cookie]; + } + } +} + +#pragma mark Accessors + +- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val { + signIn_.networkLossTimeoutInterval = val; +} + +- (NSTimeInterval)networkLossTimeoutInterval { + return signIn_.networkLossTimeoutInterval; +} + +- (BOOL)shouldUseKeychain { + NSString *name = self.keychainItemName; + return ([name length] > 0); +} + +- (BOOL)showsInitialActivityIndicator { + return (mustShowActivityIndicator_ == 1 || initialHTMLString_ == nil); +} + +- (void)setShowsInitialActivityIndicator:(BOOL)flag { + mustShowActivityIndicator_ = (flag ? 1 : -1); +} + +#pragma mark User Properties + +- (void)setProperty:(id)obj forKey:(NSString *)key { + if (obj == nil) { + // User passed in nil, so delete the property + [properties_ removeObjectForKey:key]; + } else { + // Be sure the property dictionary exists + if (properties_ == nil) { + [self setProperties:[NSMutableDictionary dictionary]]; + } + [properties_ setObject:obj forKey:key]; + } +} + +- (id)propertyForKey:(NSString *)key { + id obj = [properties_ objectForKey:key]; + + // Be sure the returned pointer has the life of the autorelease pool, + // in case self is released immediately + return [[obj retain] autorelease]; +} + +#pragma mark SignIn callbacks + +- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request { + // This is the signIn object's webRequest method, telling the controller + // to either display the request in the webview, or if the request is nil, + // to close the window. + // + // All web requests and all window closing goes through this routine + +#if DEBUG + if (self.navigationController) { + if (self.navigationController.topViewController != self && request != nil) { + NSLog(@"Unexpected: Request to show, when already on top. request %@", [request URL]); + } else if(self.navigationController.topViewController != self && request == nil) { + NSLog(@"Unexpected: Request to pop, when not on top. request nil"); + } + } +#endif + + if (request != nil) { + const NSTimeInterval kJanuary2011 = 1293840000; + BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011); + if (isDateValid) { + // Display the request. + self.request = request; + // The app may prefer some html other than blank white to be displayed + // before the sign-in web page loads. + // The first fetch might be slow, so the client programmer may want + // to show a local "loading" message. + // On iOS 5+, UIWebView will ignore loadHTMLString: if it's followed by + // a loadRequest: call, so if there is a "loading" message we defer + // the loadRequest: until after after we've drawn the "loading" message. + // + // If there is no initial html string, we show the activity indicator + // unless the user set showsInitialActivityIndicator to NO; if there + // is an initial html string, we hide the indicator unless the user set + // showsInitialActivityIndicator to YES. + NSString *html = self.initialHTMLString; + if ([html length] > 0) { + [initialActivityIndicator_ setHidden:(mustShowActivityIndicator_ < 1)]; + [self.webView loadHTMLString:html baseURL:nil]; + } else { + [initialActivityIndicator_ setHidden:(mustShowActivityIndicator_ < 0)]; + [self.webView loadRequest:request]; + } + } else { + // clock date is invalid, so signing in would fail with an unhelpful error + // from the server. Warn the user in an html string showing a watch icon, + // question mark, and the system date and time. Hopefully this will clue + // in brighter users, or at least give them a clue when they report the + // problem to developers. + // + // Even better is for apps to check the system clock and show some more + // helpful, localized instructions for users; this is really a fallback. + NSString *const html = @"<html><body><div align=center><font size='7'>" + @"⌚ ?<br><i>System Clock Incorrect</i><br>%@" + @"</font></div></body></html>"; + NSString *errHTML = [NSString stringWithFormat:html, [NSDate date]]; + + [[self webView] loadHTMLString:errHTML baseURL:nil]; + } + } else { + // request was nil. + [self popView]; + } +} + +- (void)signIn:(GTMOAuth2SignIn *)signIn + finishedWithAuth:(GTMOAuth2Authentication *)auth + error:(NSError *)error { + if (!hasCalledFinished_) { + hasCalledFinished_ = YES; + + if (error == nil) { + if (self.shouldUseKeychain) { + NSString *keychainItemName = self.keychainItemName; + if (auth.canAuthorize) { + // save the auth params in the keychain + CFTypeRef accessibility = self.keychainItemAccessibility; + [[self class] saveParamsToKeychainForName:keychainItemName + accessibility:accessibility + authentication:auth + error:NULL]; + } else { + // remove the auth params from the keychain + [[self class] removeAuthFromKeychainForName:keychainItemName]; + } + } + } + + if (delegate_ && finishedSelector_) { + SEL sel = finishedSelector_; + NSMethodSignature *sig = [delegate_ methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:delegate_]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&auth atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + } + + [delegate_ autorelease]; + delegate_ = nil; + +#if NS_BLOCKS_AVAILABLE + if (completionBlock_) { + completionBlock_(self, auth, error); + + // release the block here to avoid a retain loop on the controller + [completionBlock_ autorelease]; + completionBlock_ = nil; + } +#endif + } +} + +- (void)moveWebViewFromUnderNavigationBar { + CGRect dontCare; + CGRect webFrame = self.view.bounds; + UINavigationBar *navigationBar = self.navigationController.navigationBar; + CGRectDivide(webFrame, &dontCare, &webFrame, + navigationBar.frame.size.height, CGRectMinYEdge); + [self.webView setFrame:webFrame]; +} + +// isTranslucent is defined in iPhoneOS 3.0 on. +- (BOOL)isNavigationBarTranslucent { + UINavigationBar *navigationBar = [[self navigationController] navigationBar]; + BOOL isTranslucent = + ([navigationBar respondsToSelector:@selector(isTranslucent)] && + [navigationBar isTranslucent]); + return isTranslucent; +} + +#pragma mark - +#pragma mark Protocol implementations + +- (void)viewWillAppear:(BOOL)animated { + // See the comment on clearBrowserCookies in viewDidDisappear. + [self clearBrowserCookies]; + + if (!isViewShown_) { + isViewShown_ = YES; + if ([self isNavigationBarTranslucent]) { + [self moveWebViewFromUnderNavigationBar]; + } + if (![signIn_ startSigningIn]) { + // Can't start signing in. We must pop our view. + // UIWebview needs time to stabilize. Animations need time to complete. + // We remove ourself from the view stack after that. + [self performSelector:@selector(popView) + withObject:nil + afterDelay:0.5 + inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; + } + } + [super viewWillAppear:animated]; +} + +- (void)viewDidAppear:(BOOL)animated { + didViewAppear_ = YES; + [super viewDidAppear:animated]; +} + +- (void)viewWillDisappear:(BOOL)animated { + if (!didDismissSelf_) { + // We won't receive further webview delegate messages, so be sure the + // started loading notification is balanced, if necessary + [self notifyWithName:kGTMOAuth2WebViewStoppedLoading + webView:self.webView + kind:kGTMOAuth2WebViewCancelled]; + + // We are not popping ourselves, so presumably we are being popped by the + // navigation controller; tell the sign-in object to close up shop + // + // this will indirectly call our signIn:finishedWithAuth:error: method + // for us + [signIn_ windowWasClosed]; + +#if NS_BLOCKS_AVAILABLE + self.popViewBlock = nil; +#endif + } + + [super viewWillDisappear:animated]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + + // prevent the next sign-in from showing in the WebView that the user is + // already signed in. It's possible for the WebView to set the cookies even + // after this, so we also clear them when the view first appears. + [self clearBrowserCookies]; +} + +- (void)viewDidLayoutSubviews { + // We don't call super's version of this method because + // -[UIViewController viewDidLayoutSubviews] is documented as a no-op, that + // didn't exist before iOS 5. + [initialActivityIndicator_ setCenter:[webView_ center]]; +} + +- (BOOL)webView:(UIWebView *)webView + shouldStartLoadWithRequest:(NSURLRequest *)request + navigationType:(UIWebViewNavigationType)navigationType { + + if (!hasDoneFinalRedirect_) { + hasDoneFinalRedirect_ = [signIn_ requestRedirectedToRequest:request]; + if (hasDoneFinalRedirect_) { + // signIn has told the view to close + return NO; + } + } + return YES; +} + +- (void)updateUI { + [backButton_ setEnabled:[[self webView] canGoBack]]; + [forwardButton_ setEnabled:[[self webView] canGoForward]]; +} + +- (void)webViewDidStartLoad:(UIWebView *)webView { + [self notifyWithName:kGTMOAuth2WebViewStartedLoading + webView:webView + kind:nil]; + [self updateUI]; +} + +- (void)webViewDidFinishLoad:(UIWebView *)webView { + [self notifyWithName:kGTMOAuth2WebViewStoppedLoading + webView:webView + kind:kGTMOAuth2WebViewFinished]; + + NSString *title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"]; + if ([title length] > 0) { + [signIn_ titleChanged:title]; + } else { +#if DEBUG + // Verify that Javascript is enabled + NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"1+1"]; + NSAssert([result integerValue] == 2, @"GTMOAuth2: Javascript is required"); +#endif + } + + if (self.request && [self.initialHTMLString length] > 0) { + // The request was pending. + [self setInitialHTMLString:nil]; + [self.webView loadRequest:self.request]; + } else { + [initialActivityIndicator_ setHidden:YES]; + [signIn_ cookiesChanged:[NSHTTPCookieStorage sharedHTTPCookieStorage]]; + + [self updateUI]; + } +} + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { + [self notifyWithName:kGTMOAuth2WebViewStoppedLoading + webView:webView + kind:kGTMOAuth2WebViewFailed]; + + // Tell the sign-in object that a load failed; if it was the authorization + // URL, it will pop the view and return an error to the delegate. + if (didViewAppear_) { + BOOL isUserInterruption = ([error code] == NSURLErrorCancelled + && [[error domain] isEqual:NSURLErrorDomain]); + if (isUserInterruption) { + // Ignore this error: + // Users report that this error occurs when clicking too quickly on the + // accept button, before the page has completely loaded. Ignoring + // this error seems to provide a better experience than does immediately + // cancelling sign-in. + // + // This error also occurs whenever UIWebView is sent the stopLoading + // message, so if we ever send that message intentionally, we need to + // revisit this bypass. + return; + } + + [signIn_ loadFailedWithError:error]; + } else { + // UIWebview needs time to stabilize. Animations need time to complete. + [signIn_ performSelector:@selector(loadFailedWithError:) + withObject:error + afterDelay:0.5 + inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; + } +} + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000 +// When running on a device with an OS version < 6, this gets called. +// +// Since it is never called in iOS 6 or greater, if your min deployment +// target is iOS6 or greater, then you don't need to have this method compiled +// into your app. +// +// When running on a device with an OS version 6 or greater, this code is +// not called. - (NSUInteger)supportedInterfaceOrientations; would be called, +// if it existed. Since it is absent, +// Allow the default orientations: All for iPad, all but upside down for iPhone. +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + BOOL value = YES; + if (!isInsideShouldAutorotateToInterfaceOrientation_) { + isInsideShouldAutorotateToInterfaceOrientation_ = YES; + UIViewController *navigationController = [self navigationController]; + if (navigationController != nil) { + value = [navigationController shouldAutorotateToInterfaceOrientation:interfaceOrientation]; + } else { + value = [super shouldAutorotateToInterfaceOrientation:interfaceOrientation]; + } + isInsideShouldAutorotateToInterfaceOrientation_ = NO; + } + return value; +} +#endif + + +@end + + +#pragma mark Common Code + +@implementation GTMOAuth2Keychain + ++ (GTMOAuth2Keychain *)defaultKeychain { + if (sDefaultKeychain == nil) { + sDefaultKeychain = [[self alloc] init]; + } + return sDefaultKeychain; +} + + +// For unit tests: allow setting a mock object ++ (void)setDefaultKeychain:(GTMOAuth2Keychain *)keychain { + if (sDefaultKeychain != keychain) { + [sDefaultKeychain release]; + sDefaultKeychain = [keychain retain]; + } +} + +- (NSString *)keyForService:(NSString *)service account:(NSString *)account { + return [NSString stringWithFormat:@"com.google.GTMOAuth.%@%@", service, account]; +} + +// The Keychain API isn't available on the iPhone simulator in SDKs before 3.0, +// so, on early simulators we use a fake API, that just writes, unencrypted, to +// NSUserDefaults. +#if TARGET_IPHONE_SIMULATOR && __IPHONE_OS_VERSION_MAX_ALLOWED < 30000 +#pragma mark Simulator + +// Simulator - just simulated, not secure. +- (NSString *)passwordForService:(NSString *)service account:(NSString *)account error:(NSError **)error { + NSString *result = nil; + if (0 < [service length] && 0 < [account length]) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *key = [self keyForService:service account:account]; + result = [defaults stringForKey:key]; + if (result == nil && error != NULL) { + *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain + code:kGTMOAuth2KeychainErrorNoPassword + userInfo:nil]; + } + } else if (error != NULL) { + *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain + code:kGTMOAuth2KeychainErrorBadArguments + userInfo:nil]; + } + return result; + +} + + +// Simulator - just simulated, not secure. +- (BOOL)removePasswordForService:(NSString *)service account:(NSString *)account error:(NSError **)error { + BOOL didSucceed = NO; + if (0 < [service length] && 0 < [account length]) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *key = [self keyForService:service account:account]; + [defaults removeObjectForKey:key]; + [defaults synchronize]; + } else if (error != NULL) { + *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain + code:kGTMOAuth2KeychainErrorBadArguments + userInfo:nil]; + } + return didSucceed; +} + +// Simulator - just simulated, not secure. +- (BOOL)setPassword:(NSString *)password + forService:(NSString *)service + accessibility:(CFTypeRef)accessibility + account:(NSString *)account + error:(NSError **)error { + BOOL didSucceed = NO; + if (0 < [password length] && 0 < [service length] && 0 < [account length]) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *key = [self keyForService:service account:account]; + [defaults setObject:password forKey:key]; + [defaults synchronize]; + didSucceed = YES; + } else if (error != NULL) { + *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain + code:kGTMOAuth2KeychainErrorBadArguments + userInfo:nil]; + } + return didSucceed; +} + +#else // ! TARGET_IPHONE_SIMULATOR +#pragma mark Device + ++ (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account { + NSMutableDictionary *query = [NSMutableDictionary dictionaryWithObjectsAndKeys: + (id)kSecClassGenericPassword, (id)kSecClass, + @"OAuth", (id)kSecAttrGeneric, + account, (id)kSecAttrAccount, + service, (id)kSecAttrService, + nil]; + return query; +} + +- (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account { + return [[self class] keychainQueryForService:service account:account]; +} + + + +// iPhone +- (NSString *)passwordForService:(NSString *)service account:(NSString *)account error:(NSError **)error { + OSStatus status = kGTMOAuth2KeychainErrorBadArguments; + NSString *result = nil; + if (0 < [service length] && 0 < [account length]) { + CFDataRef passwordData = NULL; + NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account]; + [keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData]; + [keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit]; + + status = SecItemCopyMatching((CFDictionaryRef)keychainQuery, + (CFTypeRef *)&passwordData); + if (status == noErr && 0 < [(NSData *)passwordData length]) { + result = [[[NSString alloc] initWithData:(NSData *)passwordData + encoding:NSUTF8StringEncoding] autorelease]; + } + if (passwordData != NULL) { + CFRelease(passwordData); + } + } + if (status != noErr && error != NULL) { + *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain + code:status + userInfo:nil]; + } + return result; +} + + +// iPhone +- (BOOL)removePasswordForService:(NSString *)service account:(NSString *)account error:(NSError **)error { + OSStatus status = kGTMOAuth2KeychainErrorBadArguments; + if (0 < [service length] && 0 < [account length]) { + NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account]; + status = SecItemDelete((CFDictionaryRef)keychainQuery); + } + if (status != noErr && error != NULL) { + *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain + code:status + userInfo:nil]; + } + return status == noErr; +} + +// iPhone +- (BOOL)setPassword:(NSString *)password + forService:(NSString *)service + accessibility:(CFTypeRef)accessibility + account:(NSString *)account + error:(NSError **)error { + OSStatus status = kGTMOAuth2KeychainErrorBadArguments; + if (0 < [service length] && 0 < [account length]) { + [self removePasswordForService:service account:account error:nil]; + if (0 < [password length]) { + NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account]; + NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; + [keychainQuery setObject:passwordData forKey:(id)kSecValueData]; + + if (accessibility != NULL && &kSecAttrAccessible != NULL) { + [keychainQuery setObject:(id)accessibility + forKey:(id)kSecAttrAccessible]; + } + status = SecItemAdd((CFDictionaryRef)keychainQuery, NULL); + } + } + if (status != noErr && error != NULL) { + *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain + code:status + userInfo:nil]; + } + return status == noErr; +} + +#endif // ! TARGET_IPHONE_SIMULATOR + +@end + +#endif // TARGET_OS_IPHONE + +#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES diff --git a/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewTouch.xib b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewTouch.xib new file mode 100644 index 00000000..4f91fa4a --- /dev/null +++ b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewTouch.xib @@ -0,0 +1,494 @@ +<?xml version="1.0" encoding="UTF-8"?> +<archive type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="7.10"> + <data> + <int key="IBDocument.SystemTarget">1024</int> + <string key="IBDocument.SystemVersion">12C60</string> + <string key="IBDocument.InterfaceBuilderVersion">2840</string> + <string key="IBDocument.AppKitVersion">1187.34</string> + <string key="IBDocument.HIToolboxVersion">625.00</string> + <object class="NSMutableDictionary" key="IBDocument.PluginVersions"> + <string key="NS.key.0">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string key="NS.object.0">1926</string> + </object> + <object class="NSArray" key="IBDocument.IntegratedClassDependencies"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>IBProxyObject</string> + <string>IBUIActivityIndicatorView</string> + <string>IBUIBarButtonItem</string> + <string>IBUIButton</string> + <string>IBUINavigationItem</string> + <string>IBUIView</string> + <string>IBUIWebView</string> + </object> + <object class="NSArray" key="IBDocument.PluginDependencies"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</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="IBProxyObject" id="372490531"> + <string key="IBProxiedObjectIdentifier">IBFilesOwner</string> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + </object> + <object class="IBProxyObject" id="975951072"> + <string key="IBProxiedObjectIdentifier">IBFirstResponder</string> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + </object> + <object class="IBUINavigationItem" id="1047805472"> + <string key="IBUITitle">OAuth</string> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + </object> + <object class="IBUIBarButtonItem" id="961671599"> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + <int key="IBUIStyle">1</int> + </object> + <object class="IBUIView" id="808907889"> + <reference key="NSNextResponder"/> + <int key="NSvFlags">292</int> + <object class="NSMutableArray" key="NSSubviews"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBUIButton" id="453250804"> + <reference key="NSNextResponder" ref="808907889"/> + <int key="NSvFlags">292</int> + <string key="NSFrameSize">{30, 30}</string> + <reference key="NSSuperview" ref="808907889"/> + <reference key="NSWindow"/> + <reference key="NSNextKeyView" ref="981703116"/> + <bool key="IBUIOpaque">NO</bool> + <bool key="IBUIClearsContextBeforeDrawing">NO</bool> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + <int key="IBUIContentHorizontalAlignment">0</int> + <int key="IBUIContentVerticalAlignment">0</int> + <string key="IBUITitleShadowOffset">{0, -2}</string> + <string key="IBUINormalTitle">◀</string> + <object class="NSColor" key="IBUIHighlightedTitleColor" id="193465259"> + <int key="NSColorSpace">3</int> + <bytes key="NSWhite">MQA</bytes> + </object> + <object class="NSColor" key="IBUIDisabledTitleColor"> + <int key="NSColorSpace">2</int> + <bytes key="NSRGB">MC41OTYwNzg0NiAwLjY4NjI3NDUzIDAuOTUyOTQxMjQgMC42MDAwMDAwMgA</bytes> + </object> + <reference key="IBUINormalTitleColor" ref="193465259"/> + <object class="NSColor" key="IBUINormalTitleShadowColor" id="999379443"> + <int key="NSColorSpace">3</int> + <bytes key="NSWhite">MC41AA</bytes> + </object> + <object class="IBUIFontDescription" key="IBUIFontDescription" id="621440819"> + <string key="name">Helvetica-Bold</string> + <string key="family">Helvetica</string> + <int key="traits">2</int> + <double key="pointSize">24</double> + </object> + <object class="NSFont" key="IBUIFont" id="530402572"> + <string key="NSName">Helvetica-Bold</string> + <double key="NSSize">24</double> + <int key="NSfFlags">16</int> + </object> + </object> + <object class="IBUIButton" id="981703116"> + <reference key="NSNextResponder" ref="808907889"/> + <int key="NSvFlags">292</int> + <string key="NSFrame">{{30, 0}, {30, 30}}</string> + <reference key="NSSuperview" ref="808907889"/> + <reference key="NSWindow"/> + <reference key="NSNextKeyView"/> + <bool key="IBUIOpaque">NO</bool> + <bool key="IBUIClearsContextBeforeDrawing">NO</bool> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + <int key="IBUIContentHorizontalAlignment">0</int> + <int key="IBUIContentVerticalAlignment">0</int> + <string key="IBUITitleShadowOffset">{0, -2}</string> + <string key="IBUINormalTitle">▶</string> + <reference key="IBUIHighlightedTitleColor" ref="193465259"/> + <object class="NSColor" key="IBUIDisabledTitleColor"> + <int key="NSColorSpace">2</int> + <bytes key="NSRGB">MC41ODQzMTM3NSAwLjY3NDUwOTgyIDAuOTUyOTQxMjQgMC42MDAwMDAwMgA</bytes> + </object> + <reference key="IBUINormalTitleColor" ref="193465259"/> + <reference key="IBUINormalTitleShadowColor" ref="999379443"/> + <reference key="IBUIFontDescription" ref="621440819"/> + <reference key="IBUIFont" ref="530402572"/> + </object> + </object> + <string key="NSFrameSize">{60, 30}</string> + <reference key="NSSuperview"/> + <reference key="NSWindow"/> + <reference key="NSNextKeyView" ref="453250804"/> + <object class="NSColor" key="IBUIBackgroundColor"> + <int key="NSColorSpace">3</int> + <bytes key="NSWhite">MSAwAA</bytes> + </object> + <bool key="IBUIOpaque">NO</bool> + <bool key="IBUIClearsContextBeforeDrawing">NO</bool> + <object class="IBUISimulatedOrientationMetrics" key="IBUISimulatedOrientationMetrics"> + <int key="IBUIInterfaceOrientation">3</int> + <int key="interfaceOrientation">3</int> + </object> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + </object> + <object class="IBUIView" id="426018584"> + <reference key="NSNextResponder"/> + <int key="NSvFlags">274</int> + <object class="NSMutableArray" key="NSSubviews"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBUIWebView" id="663477729"> + <reference key="NSNextResponder" ref="426018584"/> + <int key="NSvFlags">274</int> + <string key="NSFrameSize">{320, 460}</string> + <reference key="NSSuperview" ref="426018584"/> + <reference key="NSWindow"/> + <reference key="NSNextKeyView" ref="268967673"/> + <object class="NSColor" key="IBUIBackgroundColor"> + <int key="NSColorSpace">1</int> + <bytes key="NSRGB">MSAxIDEAA</bytes> + </object> + <bool key="IBUIClipsSubviews">YES</bool> + <bool key="IBUIMultipleTouchEnabled">YES</bool> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + <int key="IBUIDataDetectorTypes">1</int> + <bool key="IBUIDetectsPhoneNumbers">YES</bool> + </object> + <object class="IBUIActivityIndicatorView" id="268967673"> + <reference key="NSNextResponder" ref="426018584"/> + <int key="NSvFlags">301</int> + <string key="NSFrame">{{150, 115}, {20, 20}}</string> + <reference key="NSSuperview" ref="426018584"/> + <reference key="NSWindow"/> + <reference key="NSNextKeyView"/> + <string key="NSReuseIdentifierKey">_NS:9</string> + <bool key="IBUIOpaque">NO</bool> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + <bool key="IBUIHidesWhenStopped">NO</bool> + <bool key="IBUIAnimating">YES</bool> + <int key="IBUIStyle">2</int> + </object> + </object> + <string key="NSFrameSize">{320, 460}</string> + <reference key="NSSuperview"/> + <reference key="NSWindow"/> + <reference key="NSNextKeyView" ref="663477729"/> + <object class="NSColor" key="IBUIBackgroundColor"> + <int key="NSColorSpace">3</int> + <bytes key="NSWhite">MQA</bytes> + <object class="NSColorSpace" key="NSCustomColorSpace"> + <int key="NSID">2</int> + </object> + </object> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + </object> + </object> + <object class="IBObjectContainer" key="IBDocument.Objects"> + <object class="NSMutableArray" key="connectionRecords"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchOutletConnection" key="connection"> + <string key="label">rightBarButtonItem</string> + <reference key="source" ref="372490531"/> + <reference key="destination" ref="961671599"/> + </object> + <int key="connectionID">20</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchOutletConnection" key="connection"> + <string key="label">navButtonsView</string> + <reference key="source" ref="372490531"/> + <reference key="destination" ref="808907889"/> + </object> + <int key="connectionID">22</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchOutletConnection" key="connection"> + <string key="label">backButton</string> + <reference key="source" ref="372490531"/> + <reference key="destination" ref="453250804"/> + </object> + <int key="connectionID">25</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchOutletConnection" key="connection"> + <string key="label">forwardButton</string> + <reference key="source" ref="372490531"/> + <reference key="destination" ref="981703116"/> + </object> + <int key="connectionID">26</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchOutletConnection" key="connection"> + <string key="label">view</string> + <reference key="source" ref="372490531"/> + <reference key="destination" ref="426018584"/> + </object> + <int key="connectionID">28</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchOutletConnection" key="connection"> + <string key="label">webView</string> + <reference key="source" ref="372490531"/> + <reference key="destination" ref="663477729"/> + </object> + <int key="connectionID">29</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchOutletConnection" key="connection"> + <string key="label">initialActivityIndicator</string> + <reference key="source" ref="372490531"/> + <reference key="destination" ref="268967673"/> + </object> + <int key="connectionID">33</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchOutletConnection" key="connection"> + <string key="label">delegate</string> + <reference key="source" ref="663477729"/> + <reference key="destination" ref="372490531"/> + </object> + <int key="connectionID">9</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchOutletConnection" key="connection"> + <string key="label">rightBarButtonItem</string> + <reference key="source" ref="1047805472"/> + <reference key="destination" ref="961671599"/> + </object> + <int key="connectionID">14</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchEventConnection" key="connection"> + <string key="label">goBack</string> + <reference key="source" ref="453250804"/> + <reference key="destination" ref="663477729"/> + <int key="IBEventType">7</int> + </object> + <int key="connectionID">18</int> + </object> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchEventConnection" key="connection"> + <string key="label">goForward</string> + <reference key="source" ref="981703116"/> + <reference key="destination" ref="663477729"/> + <int key="IBEventType">7</int> + </object> + <int key="connectionID">19</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">-1</int> + <reference key="object" ref="372490531"/> + <reference key="parent" ref="0"/> + <string key="objectName">File's Owner</string> + </object> + <object class="IBObjectRecord"> + <int key="objectID">-2</int> + <reference key="object" ref="975951072"/> + <reference key="parent" ref="0"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">6</int> + <reference key="object" ref="1047805472"/> + <object class="NSMutableArray" key="children"> + <bool key="EncodedWithXMLCoder">YES</bool> + </object> + <reference key="parent" ref="0"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">10</int> + <reference key="object" ref="961671599"/> + <reference key="parent" ref="0"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">15</int> + <reference key="object" ref="808907889"/> + <object class="NSMutableArray" key="children"> + <bool key="EncodedWithXMLCoder">YES</bool> + <reference ref="453250804"/> + <reference ref="981703116"/> + </object> + <reference key="parent" ref="0"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">16</int> + <reference key="object" ref="453250804"/> + <reference key="parent" ref="808907889"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">17</int> + <reference key="object" ref="981703116"/> + <reference key="parent" ref="808907889"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">27</int> + <reference key="object" ref="426018584"/> + <object class="NSMutableArray" key="children"> + <bool key="EncodedWithXMLCoder">YES</bool> + <reference ref="663477729"/> + <reference ref="268967673"/> + </object> + <reference key="parent" ref="0"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">4</int> + <reference key="object" ref="663477729"/> + <reference key="parent" ref="426018584"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">31</int> + <reference key="object" ref="268967673"/> + <reference key="parent" ref="426018584"/> + </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.CustomClassName</string> + <string>-1.IBPluginDependency</string> + <string>-2.CustomClassName</string> + <string>-2.IBPluginDependency</string> + <string>10.IBPluginDependency</string> + <string>15.IBPluginDependency</string> + <string>16.IBPluginDependency</string> + <string>17.IBPluginDependency</string> + <string>27.IBPluginDependency</string> + <string>31.IBPluginDependency</string> + <string>4.IBPluginDependency</string> + <string>6.IBPluginDependency</string> + </object> + <object class="NSArray" key="dict.values"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>GTMOAuth2ViewControllerTouch</string> + <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string>UIResponder</string> + <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</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">33</int> + </object> + <object class="IBClassDescriber" key="IBDocument.Classes"> + <object class="NSMutableArray" key="referencedPartialClassDescriptions"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBPartialClassDescription"> + <string key="className">GTMOAuth2ViewControllerTouch</string> + <string key="superclassName">UIViewController</string> + <object class="NSMutableDictionary" key="outlets"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="NSArray" key="dict.sortedKeys"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>backButton</string> + <string>forwardButton</string> + <string>initialActivityIndicator</string> + <string>navButtonsView</string> + <string>rightBarButtonItem</string> + <string>webView</string> + </object> + <object class="NSArray" key="dict.values"> + <bool key="EncodedWithXMLCoder">YES</bool> + <string>UIButton</string> + <string>UIButton</string> + <string>UIActivityIndicatorView</string> + <string>UIView</string> + <string>UIBarButtonItem</string> + <string>UIWebView</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>backButton</string> + <string>forwardButton</string> + <string>initialActivityIndicator</string> + <string>navButtonsView</string> + <string>rightBarButtonItem</string> + <string>webView</string> + </object> + <object class="NSArray" key="dict.values"> + <bool key="EncodedWithXMLCoder">YES</bool> + <object class="IBToOneOutletInfo"> + <string key="name">backButton</string> + <string key="candidateClassName">UIButton</string> + </object> + <object class="IBToOneOutletInfo"> + <string key="name">forwardButton</string> + <string key="candidateClassName">UIButton</string> + </object> + <object class="IBToOneOutletInfo"> + <string key="name">initialActivityIndicator</string> + <string key="candidateClassName">UIActivityIndicatorView</string> + </object> + <object class="IBToOneOutletInfo"> + <string key="name">navButtonsView</string> + <string key="candidateClassName">UIView</string> + </object> + <object class="IBToOneOutletInfo"> + <string key="name">rightBarButtonItem</string> + <string key="candidateClassName">UIBarButtonItem</string> + </object> + <object class="IBToOneOutletInfo"> + <string key="name">webView</string> + <string key="candidateClassName">UIWebView</string> + </object> + </object> + </object> + <object class="IBClassDescriptionSource" key="sourceIdentifier"> + <string key="majorKey">IBProjectSource</string> + <string key="minorKey">./Classes/GTMOAuth2ViewControllerTouch.h</string> + </object> + </object> + </object> + </object> + <int key="IBDocument.localizationMode">0</int> + <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaTouchFramework</string> + <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies"> + <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS</string> + <real value="1024" key="NS.object.0"/> + </object> + <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencyDefaults"> + <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS</string> + <real value="1536" key="NS.object.0"/> + </object> + <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies"> + <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3</string> + <integer value="3000" key="NS.object.0"/> + </object> + <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool> + <int key="IBDocument.defaultPropertyAccessControl">3</int> + <string key="IBCocoaTouchPluginVersion">1926</string> + </data> +</archive> diff --git a/example/ios/iOS UI Test/iOS UI Test.xcodeproj/project.pbxproj b/example/ios/iOS UI Test/iOS UI Test.xcodeproj/project.pbxproj index db280fde..e9cd3797 100644 --- a/example/ios/iOS UI Test/iOS UI Test.xcodeproj/project.pbxproj +++ b/example/ios/iOS UI Test/iOS UI Test.xcodeproj/project.pbxproj @@ -28,6 +28,13 @@ ABE4026B173F3FCE007F1FB3 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABE4026A173F3FCE007F1FB3 /* ImageIO.framework */; }; B12AAA3417322654003551C7 /* MCOMessageView.mm in Sources */ = {isa = PBXBuildFile; fileRef = B12AAA3017322654003551C7 /* MCOMessageView.mm */; }; B12AAA3517322654003551C7 /* MCTMsgViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = B12AAA3217322654003551C7 /* MCTMsgViewController.mm */; }; + C6D7194D178BB8B4008ED15F /* GTMHTTPFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D71939178BA812008ED15F /* GTMHTTPFetcher.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + C6D7194E178BB8B4008ED15F /* GTMHTTPFetchHistory.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D7193B178BA812008ED15F /* GTMHTTPFetchHistory.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + C6D7194F178BB8B4008ED15F /* GTMOAuth2Authentication.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D7193E178BA812008ED15F /* GTMOAuth2Authentication.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + C6D71950178BB8B4008ED15F /* GTMOAuth2SignIn.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D71940178BA812008ED15F /* GTMOAuth2SignIn.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + C6D71951178BB8B4008ED15F /* GTMOAuth2ViewControllerTouch.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D71947178BA812008ED15F /* GTMOAuth2ViewControllerTouch.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + C6D71952178BB8B4008ED15F /* GTMOAuth2ViewTouch.xib in Resources */ = {isa = PBXBuildFile; fileRef = C6D71948178BA812008ED15F /* GTMOAuth2ViewTouch.xib */; }; + C6D71954178BB91E008ED15F /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6D71953178BB91E008ED15F /* SystemConfiguration.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -107,6 +114,18 @@ B12AAA3017322654003551C7 /* MCOMessageView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MCOMessageView.mm; sourceTree = "<group>"; }; B12AAA3117322654003551C7 /* MCTMsgViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MCTMsgViewController.h; sourceTree = "<group>"; }; B12AAA3217322654003551C7 /* MCTMsgViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MCTMsgViewController.mm; sourceTree = "<group>"; }; + C6D71938178BA812008ED15F /* GTMHTTPFetcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMHTTPFetcher.h; sourceTree = "<group>"; }; + C6D71939178BA812008ED15F /* GTMHTTPFetcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMHTTPFetcher.m; sourceTree = "<group>"; }; + C6D7193A178BA812008ED15F /* GTMHTTPFetchHistory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMHTTPFetchHistory.h; sourceTree = "<group>"; }; + C6D7193B178BA812008ED15F /* GTMHTTPFetchHistory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMHTTPFetchHistory.m; sourceTree = "<group>"; }; + C6D7193D178BA812008ED15F /* GTMOAuth2Authentication.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2Authentication.h; sourceTree = "<group>"; }; + C6D7193E178BA812008ED15F /* GTMOAuth2Authentication.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2Authentication.m; sourceTree = "<group>"; }; + C6D7193F178BA812008ED15F /* GTMOAuth2SignIn.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2SignIn.h; sourceTree = "<group>"; }; + C6D71940178BA812008ED15F /* GTMOAuth2SignIn.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2SignIn.m; sourceTree = "<group>"; }; + C6D71946178BA812008ED15F /* GTMOAuth2ViewControllerTouch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2ViewControllerTouch.h; sourceTree = "<group>"; }; + C6D71947178BA812008ED15F /* GTMOAuth2ViewControllerTouch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2ViewControllerTouch.m; sourceTree = "<group>"; }; + C6D71948178BA812008ED15F /* GTMOAuth2ViewTouch.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = GTMOAuth2ViewTouch.xib; sourceTree = "<group>"; }; + C6D71953178BB91E008ED15F /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -114,6 +133,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C6D71954178BB91E008ED15F /* SystemConfiguration.framework in Frameworks */, AB7BA446171389CC00104953 /* libMailCore-ios.a in Frameworks */, ABE4026B173F3FCE007F1FB3 /* ImageIO.framework in Frameworks */, AB9EAE40170374D900D750C7 /* Security.framework in Frameworks */, @@ -170,6 +190,7 @@ AB9EAE04170368F000D750C7 /* Frameworks */ = { isa = PBXGroup; children = ( + C6D71953178BB91E008ED15F /* SystemConfiguration.framework */, ABE4026A173F3FCE007F1FB3 /* ImageIO.framework */, AB7BA4471713913F00104953 /* CFNetwork.framework */, AB9EAE3F170374D900D750C7 /* Security.framework */, @@ -183,6 +204,7 @@ AB9EAE0B170368F000D750C7 /* iOS UI Test */ = { isa = PBXGroup; children = ( + C6D71936178BA812008ED15F /* gtm-oauth2 */, AB7BA4321713898B00104953 /* mailcore2.xcodeproj */, AB9EAE11170368F000D750C7 /* main.mm */, AB9EAE14170368F000D750C7 /* AppDelegate.h */, @@ -217,6 +239,49 @@ name = "Supporting Files"; sourceTree = "<group>"; }; + C6D71936178BA812008ED15F /* gtm-oauth2 */ = { + isa = PBXGroup; + children = ( + C6D71937178BA812008ED15F /* HTTPFetcher */, + C6D7193C178BA812008ED15F /* Source */, + ); + name = "gtm-oauth2"; + path = "../../../common/gtm-oauth2"; + sourceTree = "<group>"; + }; + C6D71937178BA812008ED15F /* HTTPFetcher */ = { + isa = PBXGroup; + children = ( + C6D71938178BA812008ED15F /* GTMHTTPFetcher.h */, + C6D71939178BA812008ED15F /* GTMHTTPFetcher.m */, + C6D7193A178BA812008ED15F /* GTMHTTPFetchHistory.h */, + C6D7193B178BA812008ED15F /* GTMHTTPFetchHistory.m */, + ); + path = HTTPFetcher; + sourceTree = "<group>"; + }; + C6D7193C178BA812008ED15F /* Source */ = { + isa = PBXGroup; + children = ( + C6D7193D178BA812008ED15F /* GTMOAuth2Authentication.h */, + C6D7193E178BA812008ED15F /* GTMOAuth2Authentication.m */, + C6D7193F178BA812008ED15F /* GTMOAuth2SignIn.h */, + C6D71940178BA812008ED15F /* GTMOAuth2SignIn.m */, + C6D71945178BA812008ED15F /* Touch */, + ); + path = Source; + sourceTree = "<group>"; + }; + C6D71945178BA812008ED15F /* Touch */ = { + isa = PBXGroup; + children = ( + C6D71946178BA812008ED15F /* GTMOAuth2ViewControllerTouch.h */, + C6D71947178BA812008ED15F /* GTMOAuth2ViewControllerTouch.m */, + C6D71948178BA812008ED15F /* GTMOAuth2ViewTouch.xib */, + ); + path = Touch; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -313,6 +378,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C6D71952178BB8B4008ED15F /* GTMOAuth2ViewTouch.xib in Resources */, ABE40269173F0282007F1FB3 /* MCOMessageViewScript.js in Resources */, AB9EAE10170368F000D750C7 /* InfoPlist.strings in Resources */, AB9EAE18170368F000D750C7 /* Default.png in Resources */, @@ -331,10 +397,15 @@ buildActionMask = 2147483647; files = ( AB9EAE12170368F000D750C7 /* main.mm in Sources */, + C6D7194E178BB8B4008ED15F /* GTMHTTPFetchHistory.m in Sources */, AB9EAE16170368F000D750C7 /* AppDelegate.m in Sources */, AB9EAE22170368F000D750C7 /* MasterViewController.m in Sources */, AB9EAE3617036FD700D750C7 /* SettingsViewController.m in Sources */, + C6D7194D178BB8B4008ED15F /* GTMHTTPFetcher.m in Sources */, + C6D71951178BB8B4008ED15F /* GTMOAuth2ViewControllerTouch.m in Sources */, AB665BCD17134336007F2151 /* FXKeychain.m in Sources */, + C6D71950178BB8B4008ED15F /* GTMOAuth2SignIn.m in Sources */, + C6D7194F178BB8B4008ED15F /* GTMOAuth2Authentication.m in Sources */, B12AAA3417322654003551C7 /* MCOMessageView.mm in Sources */, B12AAA3517322654003551C7 /* MCTMsgViewController.mm in Sources */, ); diff --git a/example/ios/iOS UI Test/iOS UI Test/MasterViewController.m b/example/ios/iOS UI Test/iOS UI Test/MasterViewController.m index 3e5e4422..5bbc0d53 100644 --- a/example/ios/iOS UI Test/iOS UI Test/MasterViewController.m +++ b/example/ios/iOS UI Test/iOS UI Test/MasterViewController.m @@ -10,6 +10,11 @@ #import <MailCore/MailCore.h> #import "FXKeychain.h" #import "MCTMsgViewController.h" +#import "GTMOAuth2ViewControllerTouch.h" + +#define CLIENT_ID @"the-client-id" +#define CLIENT_SECRET @"the-client-secret" +#define KEYCHAIN_ITEM_NAME @"MailCore OAuth 2.0 Token" @interface MasterViewController () @property (nonatomic, strong) NSArray *messages; @@ -26,24 +31,78 @@ [[NSUserDefaults standardUserDefaults] registerDefaults:@{ HostnameKey: @"imap.gmail.com" }]; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"OAuth2Enabled"]) { + [self startOAuth2]; + } + else { + [self startLogin]; + } +} + +- (void) startLogin +{ NSString *username = [[NSUserDefaults standardUserDefaults] objectForKey:UsernameKey]; NSString *password = [[FXKeychain defaultKeychain] objectForKey:PasswordKey]; NSString *hostname = [[NSUserDefaults standardUserDefaults] objectForKey:HostnameKey]; - - [self loadAccountWithUsername:username password:password hostname:hostname]; + + if (!username.length || !password.length) { + [self performSelector:@selector(showSettingsViewController:) withObject:nil afterDelay:0.5]; + return; + } + + [self loadAccountWithUsername:username password:password hostname:hostname oauth2Token:nil]; } -- (void)loadAccountWithUsername:(NSString *)username password:(NSString *)password hostname:(NSString *)hostname { - if (!username.length || !password.length) { - [self performSelector:@selector(showSettingsViewController:) withObject:nil afterDelay:0.5]; - return; - } - +- (void) startOAuth2 +{ + GTMOAuth2Authentication * auth = [GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:KEYCHAIN_ITEM_NAME + clientID:CLIENT_ID + clientSecret:CLIENT_SECRET]; + + if ([auth refreshToken] == nil) { + MasterViewController * __weak weakSelf = self; + GTMOAuth2ViewControllerTouch *viewController = [GTMOAuth2ViewControllerTouch controllerWithScope:@"https://mail.google.com/" + clientID:CLIENT_ID + clientSecret:CLIENT_SECRET + keychainItemName:KEYCHAIN_ITEM_NAME + completionHandler:^(GTMOAuth2ViewControllerTouch *viewController, GTMOAuth2Authentication *retrievedAuth, NSError *error) { + [weakSelf loadWithAuth:retrievedAuth]; + }]; + [self.navigationController pushViewController:viewController + animated:YES]; + } + else { + [auth beginTokenFetchWithDelegate:self + didFinishSelector:@selector(auth:finishedRefreshWithFetcher:error:)]; + } +} + +- (void)auth:(GTMOAuth2Authentication *)auth +finishedRefreshWithFetcher:(GTMHTTPFetcher *)fetcher + error:(NSError *)error { + [self loadWithAuth:auth]; +} + +- (void)loadWithAuth:(GTMOAuth2Authentication *)auth +{ + NSString *hostname = [[NSUserDefaults standardUserDefaults] objectForKey:HostnameKey]; + [self loadAccountWithUsername:[auth userEmail] password:nil hostname:hostname oauth2Token:[auth accessToken]]; +} + +- (void)loadAccountWithUsername:(NSString *)username + password:(NSString *)password + hostname:(NSString *)hostname + oauth2Token:(NSString *)oauth2Token +{ self.imapSession = [[MCOIMAPSession alloc] init]; self.imapSession.hostname = hostname; self.imapSession.port = 993; self.imapSession.username = username; self.imapSession.password = password; + if (oauth2Token != nil) { + self.imapSession.OAuth2Token = oauth2Token; + self.imapSession.authType = MCOAuthTypeXOAuth2; + } self.imapSession.connectionType = MCOConnectionTypeTLS; MasterViewController * __weak weakSelf = self; self.imapSession.connectionLogger = ^(void * connectionID, MCOConnectionLogType type, NSData * data) { @@ -135,7 +194,7 @@ ![password isEqualToString:self.imapSession.password] || ![hostname isEqualToString:self.imapSession.hostname]) { self.imapSession = nil; - [self loadAccountWithUsername:username password:password hostname:hostname]; + [self loadAccountWithUsername:username password:password hostname:hostname oauth2Token:nil]; } } diff --git a/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.h b/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.h index e5f5da48..a1cb84a2 100644 --- a/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.h +++ b/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.h @@ -12,6 +12,7 @@ extern NSString * const UsernameKey; extern NSString * const PasswordKey; extern NSString * const HostnameKey; extern NSString * const FetchFullMessageKey; +extern NSString * const OAuthEnabledKey; @protocol SettingsViewControllerDelegate; @@ -21,6 +22,8 @@ extern NSString * const FetchFullMessageKey; @property (weak, nonatomic) IBOutlet UITextField *passwordTextField; @property (weak, nonatomic) IBOutlet UITextField *hostnameTextField; @property (weak, nonatomic) IBOutlet UISwitch *fetchFullMessageSwitch; +@property (weak, nonatomic) IBOutlet UISwitch *useOAuth2Switch; + @property (nonatomic, weak) id<SettingsViewControllerDelegate> delegate; - (IBAction)done:(id)sender; diff --git a/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.m b/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.m index e2f0d2c5..d17b479a 100644 --- a/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.m +++ b/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.m @@ -13,6 +13,7 @@ NSString * const UsernameKey = @"username"; NSString * const PasswordKey = @"password"; NSString * const HostnameKey = @"hostname"; NSString * const FetchFullMessageKey = @"FetchFullMessageEnabled"; +NSString * const OAuthEnabledKey = @"OAuth2Enabled"; @implementation SettingsViewController @@ -21,6 +22,7 @@ NSString * const FetchFullMessageKey = @"FetchFullMessageEnabled"; [[FXKeychain defaultKeychain] setObject:self.passwordTextField.text ?: @"" forKey:PasswordKey]; [[NSUserDefaults standardUserDefaults] setObject:self.hostnameTextField.text ?: @"" forKey:HostnameKey]; [[NSUserDefaults standardUserDefaults] setBool:[self.fetchFullMessageSwitch isOn] forKey:FetchFullMessageKey]; + [[NSUserDefaults standardUserDefaults] setBool:[self.useOAuth2Switch isOn] forKey:OAuthEnabledKey]; [self.delegate settingsViewControllerFinished:self]; } @@ -33,6 +35,7 @@ NSString * const FetchFullMessageKey = @"FetchFullMessageEnabled"; self.passwordTextField.text = [[FXKeychain defaultKeychain] objectForKey:PasswordKey]; self.hostnameTextField.text = [[NSUserDefaults standardUserDefaults] stringForKey:HostnameKey]; self.fetchFullMessageSwitch.on = [[NSUserDefaults standardUserDefaults] boolForKey:FetchFullMessageKey]; + self.useOAuth2Switch.on = [[NSUserDefaults standardUserDefaults] boolForKey:OAuthEnabledKey]; } @end diff --git a/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.xib b/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.xib index 3de561bd..c3f4f089 100644 --- a/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.xib +++ b/example/ios/iOS UI Test/iOS UI Test/SettingsViewController.xib @@ -3,12 +3,12 @@ <data> <int key="IBDocument.SystemTarget">1552</int> <string key="IBDocument.SystemVersion">12E55</string> - <string key="IBDocument.InterfaceBuilderVersion">3084</string> + <string key="IBDocument.InterfaceBuilderVersion">4457.6</string> <string key="IBDocument.AppKitVersion">1187.39</string> <string key="IBDocument.HIToolboxVersion">626.00</string> <object class="NSMutableDictionary" key="IBDocument.PluginVersions"> <string key="NS.key.0">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> - <string key="NS.object.0">2083</string> + <string key="NS.object.0">3682.6</string> </object> <array key="IBDocument.IntegratedClassDependencies"> <string>IBNSLayoutConstraint</string> @@ -96,8 +96,8 @@ <int key="type">1</int> <double key="pointSize">14</double> </object> - <object class="NSFont" key="IBUIFont" id="649714166"> - <string key="NSName">Helvetica</string> + <object class="NSFont" key="IBUIFont" id="577736679"> + <string key="NSName">HelveticaNeue</string> <double key="NSSize">14</double> <int key="NSfFlags">16</int> </object> @@ -130,7 +130,7 @@ </object> <int key="IBUIClearButtonMode">1</int> <reference key="IBUIFontDescription" ref="985700861"/> - <reference key="IBUIFont" ref="649714166"/> + <reference key="IBUIFont" ref="577736679"/> </object> <object class="IBUITextField" id="394260873"> <reference key="NSNextResponder" ref="191373211"/> @@ -159,7 +159,7 @@ </object> <int key="IBUIClearButtonMode">1</int> <reference key="IBUIFontDescription" ref="985700861"/> - <reference key="IBUIFont" ref="649714166"/> + <reference key="IBUIFont" ref="577736679"/> </object> <object class="IBUISwitch" id="348875194"> <reference key="NSNextResponder" ref="191373211"/> @@ -167,7 +167,7 @@ <string key="NSFrame">{{208, 180}, {94, 27}}</string> <reference key="NSSuperview" ref="191373211"/> <reference key="NSWindow"/> - <reference key="NSNextKeyView"/> + <reference key="NSNextKeyView" ref="780270730"/> <string key="NSReuseIdentifierKey">_NS:9</string> <bool key="IBUIOpaque">NO</bool> <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> @@ -188,7 +188,7 @@ <bool key="IBUIUserInteractionEnabled">NO</bool> <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> <string key="IBUIText">Fetch full message</string> - <object class="NSColor" key="IBUITextColor"> + <object class="NSColor" key="IBUITextColor" id="471454649"> <int key="NSColorSpace">1</int> <bytes key="NSRGB">MCAwIDAAA</bytes> <string key="IBUIColorCocoaTouchKeyPath">darkTextColor</string> @@ -196,7 +196,41 @@ <nil key="IBUIHighlightedColor"/> <int key="IBUIBaselineAdjustment">0</int> <reference key="IBUIFontDescription" ref="985700861"/> - <reference key="IBUIFont" ref="649714166"/> + <reference key="IBUIFont" ref="577736679"/> + <bool key="IBUIAdjustsFontSizeToFit">NO</bool> + </object> + <object class="IBUISwitch" id="480060909"> + <reference key="NSNextResponder" ref="191373211"/> + <int key="NSvFlags">292</int> + <string key="NSFrame">{{208, 215}, {94, 27}}</string> + <reference key="NSSuperview" ref="191373211"/> + <reference key="NSWindow"/> + <reference key="NSNextKeyView"/> + <string key="NSReuseIdentifierKey">_NS:9</string> + <bool key="IBUIOpaque">NO</bool> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + <int key="IBUIContentHorizontalAlignment">0</int> + <int key="IBUIContentVerticalAlignment">0</int> + </object> + <object class="IBUILabel" id="780270730"> + <reference key="NSNextResponder" ref="191373211"/> + <int key="NSvFlags">292</int> + <string key="NSFrame">{{20, 218}, {143, 21}}</string> + <reference key="NSSuperview" ref="191373211"/> + <reference key="NSWindow"/> + <reference key="NSNextKeyView" ref="480060909"/> + <string key="NSReuseIdentifierKey">_NS:9</string> + <bool key="IBUIOpaque">NO</bool> + <bool key="IBUIClipsSubviews">YES</bool> + <int key="IBUIContentMode">7</int> + <bool key="IBUIUserInteractionEnabled">NO</bool> + <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string> + <string key="IBUIText">Use OAuth 2.0</string> + <reference key="IBUITextColor" ref="471454649"/> + <nil key="IBUIHighlightedColor"/> + <int key="IBUIBaselineAdjustment">0</int> + <reference key="IBUIFontDescription" ref="985700861"/> + <reference key="IBUIFont" ref="577736679"/> <bool key="IBUIAdjustsFontSizeToFit">NO</bool> </object> </array> @@ -272,6 +306,14 @@ <int key="connectionID">69</int> </object> <object class="IBConnectionRecord"> + <object class="IBCocoaTouchOutletConnection" key="connection"> + <string key="label">useOAuth2Switch</string> + <reference key="source" ref="372490531"/> + <reference key="destination" ref="480060909"/> + </object> + <int key="connectionID">84</int> + </object> + <object class="IBConnectionRecord"> <object class="IBCocoaTouchEventConnection" key="connection"> <string key="label">done:</string> <reference key="source" ref="38144743"/> @@ -292,6 +334,57 @@ <int key="objectID">1</int> <reference key="object" ref="191373211"/> <array class="NSMutableArray" key="children"> + <object class="IBNSLayoutConstraint" id="728173208"> + <reference key="firstItem" ref="480060909"/> + <int key="firstAttribute">5</int> + <int key="relation">0</int> + <reference key="secondItem" ref="348875194"/> + <int key="secondAttribute">5</int> + <float key="multiplier">1</float> + <object class="IBLayoutConstant" key="constant"> + <double key="value">0.0</double> + </object> + <float key="priority">1000</float> + <reference key="containingView" ref="191373211"/> + <int key="scoringType">6</int> + <float key="scoringTypeFloat">24</float> + <int key="contentType">2</int> + <bool key="placeholder">NO</bool> + </object> + <object class="IBNSLayoutConstraint" id="390655325"> + <reference key="firstItem" ref="480060909"/> + <int key="firstAttribute">3</int> + <int key="relation">0</int> + <reference key="secondItem" ref="348875194"/> + <int key="secondAttribute">4</int> + <float key="multiplier">1</float> + <object class="IBNSLayoutSymbolicConstant" key="constant"> + <double key="value">8</double> + </object> + <float key="priority">1000</float> + <reference key="containingView" ref="191373211"/> + <int key="scoringType">6</int> + <float key="scoringTypeFloat">24</float> + <int key="contentType">3</int> + <bool key="placeholder">NO</bool> + </object> + <object class="IBNSLayoutConstraint" id="999557578"> + <reference key="firstItem" ref="480060909"/> + <int key="firstAttribute">10</int> + <int key="relation">0</int> + <reference key="secondItem" ref="780270730"/> + <int key="secondAttribute">10</int> + <float key="multiplier">1</float> + <object class="IBLayoutConstant" key="constant"> + <double key="value">0.0</double> + </object> + <float key="priority">1000</float> + <reference key="containingView" ref="191373211"/> + <int key="scoringType">6</int> + <float key="scoringTypeFloat">24</float> + <int key="contentType">2</int> + <bool key="placeholder">NO</bool> + </object> <object class="IBNSLayoutConstraint" id="148058045"> <reference key="firstItem" ref="348875194"/> <int key="firstAttribute">3</int> @@ -307,6 +400,7 @@ <int key="scoringType">6</int> <float key="scoringTypeFloat">24</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="719571487"> <reference key="firstItem" ref="191373211"/> @@ -320,9 +414,44 @@ </object> <float key="priority">1000</float> <reference key="containingView" ref="191373211"/> - <int key="scoringType">8</int> + <int key="scoringType">0</int> <float key="scoringTypeFloat">29</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> + </object> + <object class="IBNSLayoutConstraint" id="247680247"> + <reference key="firstItem" ref="780270730"/> + <int key="firstAttribute">6</int> + <int key="relation">0</int> + <reference key="secondItem" ref="484795553"/> + <int key="secondAttribute">6</int> + <float key="multiplier">1</float> + <object class="IBLayoutConstant" key="constant"> + <double key="value">0.0</double> + </object> + <float key="priority">1000</float> + <reference key="containingView" ref="191373211"/> + <int key="scoringType">6</int> + <float key="scoringTypeFloat">24</float> + <int key="contentType">2</int> + <bool key="placeholder">NO</bool> + </object> + <object class="IBNSLayoutConstraint" id="986729151"> + <reference key="firstItem" ref="780270730"/> + <int key="firstAttribute">5</int> + <int key="relation">0</int> + <reference key="secondItem" ref="484795553"/> + <int key="secondAttribute">5</int> + <float key="multiplier">1</float> + <object class="IBLayoutConstant" key="constant"> + <double key="value">0.0</double> + </object> + <float key="priority">1000</float> + <reference key="containingView" ref="191373211"/> + <int key="scoringType">6</int> + <float key="scoringTypeFloat">24</float> + <int key="contentType">2</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="918242346"> <reference key="firstItem" ref="484795553"/> @@ -339,6 +468,7 @@ <int key="scoringType">6</int> <float key="scoringTypeFloat">24</float> <int key="contentType">2</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="342906512"> <reference key="firstItem" ref="484795553"/> @@ -352,9 +482,10 @@ </object> <float key="priority">1000</float> <reference key="containingView" ref="191373211"/> - <int key="scoringType">8</int> + <int key="scoringType">0</int> <float key="scoringTypeFloat">29</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="1036238214"> <reference key="firstItem" ref="191373211"/> @@ -368,9 +499,10 @@ </object> <float key="priority">1000</float> <reference key="containingView" ref="191373211"/> - <int key="scoringType">8</int> + <int key="scoringType">0</int> <float key="scoringTypeFloat">29</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="662191761"> <reference key="firstItem" ref="394260873"/> @@ -384,9 +516,10 @@ </object> <float key="priority">1000</float> <reference key="containingView" ref="191373211"/> - <int key="scoringType">8</int> + <int key="scoringType">0</int> <float key="scoringTypeFloat">29</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="457196062"> <reference key="firstItem" ref="394260873"/> @@ -403,6 +536,7 @@ <int key="scoringType">6</int> <float key="scoringTypeFloat">24</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="295413753"> <reference key="firstItem" ref="923634912"/> @@ -419,6 +553,7 @@ <int key="scoringType">6</int> <float key="scoringTypeFloat">24</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="144109937"> <reference key="firstItem" ref="191373211"/> @@ -432,9 +567,10 @@ </object> <float key="priority">1000</float> <reference key="containingView" ref="191373211"/> - <int key="scoringType">8</int> + <int key="scoringType">0</int> <float key="scoringTypeFloat">29</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="620844127"> <reference key="firstItem" ref="923634912"/> @@ -448,9 +584,10 @@ </object> <float key="priority">1000</float> <reference key="containingView" ref="191373211"/> - <int key="scoringType">8</int> + <int key="scoringType">0</int> <float key="scoringTypeFloat">29</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="321610222"> <reference key="firstItem" ref="191373211"/> @@ -464,9 +601,10 @@ </object> <float key="priority">1000</float> <reference key="containingView" ref="191373211"/> - <int key="scoringType">8</int> + <int key="scoringType">0</int> <float key="scoringTypeFloat">29</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="126503603"> <reference key="firstItem" ref="339913914"/> @@ -483,6 +621,7 @@ <int key="scoringType">3</int> <float key="scoringTypeFloat">9</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="17620276"> <reference key="firstItem" ref="339913914"/> @@ -496,9 +635,10 @@ </object> <float key="priority">1000</float> <reference key="containingView" ref="191373211"/> - <int key="scoringType">8</int> + <int key="scoringType">0</int> <float key="scoringTypeFloat">29</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="118803530"> <reference key="firstItem" ref="703582545"/> @@ -512,9 +652,10 @@ </object> <float key="priority">1000</float> <reference key="containingView" ref="191373211"/> - <int key="scoringType">8</int> + <int key="scoringType">0</int> <float key="scoringTypeFloat">29</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="7756024"> <reference key="firstItem" ref="703582545"/> @@ -528,9 +669,10 @@ </object> <float key="priority">1000</float> <reference key="containingView" ref="191373211"/> - <int key="scoringType">8</int> + <int key="scoringType">0</int> <float key="scoringTypeFloat">29</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="630587606"> <reference key="firstItem" ref="703582545"/> @@ -544,9 +686,10 @@ </object> <float key="priority">1000</float> <reference key="containingView" ref="191373211"/> - <int key="scoringType">8</int> + <int key="scoringType">0</int> <float key="scoringTypeFloat">29</float> <int key="contentType">3</int> + <bool key="placeholder">NO</bool> </object> <reference ref="703582545"/> <reference ref="339913914"/> @@ -554,6 +697,8 @@ <reference ref="394260873"/> <reference ref="348875194"/> <reference ref="484795553"/> + <reference ref="480060909"/> + <reference ref="780270730"/> </array> <reference key="parent" ref="0"/> </object> @@ -699,6 +844,7 @@ <int key="scoringType">3</int> <float key="scoringTypeFloat">9</float> <int key="contentType">1</int> + <bool key="placeholder">NO</bool> </object> <object class="IBNSLayoutConstraint" id="670227139"> <reference key="firstItem" ref="484795553"/> @@ -715,6 +861,7 @@ <int key="scoringType">3</int> <float key="scoringTypeFloat">9</float> <int key="contentType">1</int> + <bool key="placeholder">NO</bool> </object> </array> <reference key="parent" ref="191373211"/> @@ -739,6 +886,65 @@ <reference key="object" ref="269062633"/> <reference key="parent" ref="484795553"/> </object> + <object class="IBObjectRecord"> + <int key="objectID">72</int> + <reference key="object" ref="480060909"/> + <reference key="parent" ref="191373211"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">73</int> + <reference key="object" ref="780270730"/> + <array class="NSMutableArray" key="children"> + <object class="IBNSLayoutConstraint" id="725298023"> + <reference key="firstItem" ref="780270730"/> + <int key="firstAttribute">8</int> + <int key="relation">0</int> + <nil key="secondItem"/> + <int key="secondAttribute">0</int> + <float key="multiplier">1</float> + <object class="IBLayoutConstant" key="constant"> + <double key="value">21</double> + </object> + <float key="priority">1000</float> + <reference key="containingView" ref="780270730"/> + <int key="scoringType">3</int> + <float key="scoringTypeFloat">9</float> + <int key="contentType">1</int> + <bool key="placeholder">NO</bool> + </object> + </array> + <reference key="parent" ref="191373211"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">75</int> + <reference key="object" ref="725298023"/> + <reference key="parent" ref="780270730"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">76</int> + <reference key="object" ref="999557578"/> + <reference key="parent" ref="191373211"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">80</int> + <reference key="object" ref="390655325"/> + <reference key="parent" ref="191373211"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">81</int> + <reference key="object" ref="986729151"/> + <reference key="parent" ref="191373211"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">82</int> + <reference key="object" ref="247680247"/> + <reference key="parent" ref="191373211"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">83</int> + <reference key="object" ref="728173208"/> + <reference key="parent" ref="191373211"/> + </object> </array> </object> <dictionary class="NSMutableDictionary" key="flattenedProperties"> @@ -747,7 +953,7 @@ <string key="-2.CustomClassName">UIResponder</string> <string key="-2.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> <string key="1.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> - <array key="1.IBViewMetadataConstraints"> + <array class="NSMutableArray" key="1.IBViewMetadataConstraints"> <reference ref="630587606"/> <reference ref="7756024"/> <reference ref="118803530"/> @@ -762,8 +968,13 @@ <reference ref="1036238214"/> <reference ref="342906512"/> <reference ref="918242346"/> + <reference ref="986729151"/> + <reference ref="247680247"/> <reference ref="719571487"/> <reference ref="148058045"/> + <reference ref="999557578"/> + <reference ref="390655325"/> + <reference ref="728173208"/> </array> <string key="10.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> <string key="11.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> @@ -800,13 +1011,26 @@ <string key="61.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> <string key="70.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> <string key="71.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string key="72.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <boolean value="NO" key="72.IBViewMetadataTranslatesAutoresizingMaskIntoConstraints"/> + <string key="73.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <array class="NSMutableArray" key="73.IBViewMetadataConstraints"> + <reference ref="725298023"/> + </array> + <boolean value="NO" key="73.IBViewMetadataTranslatesAutoresizingMaskIntoConstraints"/> + <string key="75.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string key="76.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string key="80.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string key="81.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string key="82.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string key="83.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> <string key="9.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> </dictionary> <dictionary class="NSMutableDictionary" key="unlocalizedProperties"/> <nil key="activeLocalization"/> <dictionary class="NSMutableDictionary" key="localizations"/> <nil key="sourceID"/> - <int key="maxID">71</int> + <int key="maxID">84</int> </object> <object class="IBClassDescriber" key="IBDocument.Classes"> <array class="NSMutableArray" key="referencedPartialClassDescriptions"> @@ -837,6 +1061,7 @@ <string key="fetchFullMessageSwitch">UISwitch</string> <string key="hostnameTextField">UITextField</string> <string key="passwordTextField">UITextField</string> + <string key="useOAuth2Switch">UISwitch</string> </dictionary> <dictionary class="NSMutableDictionary" key="toOneOutletInfosByName"> <object class="IBToOneOutletInfo" key="emailTextField"> @@ -855,6 +1080,10 @@ <string key="name">passwordTextField</string> <string key="candidateClassName">UITextField</string> </object> + <object class="IBToOneOutletInfo" key="useOAuth2Switch"> + <string key="name">useOAuth2Switch</string> + <string key="candidateClassName">UISwitch</string> + </object> </dictionary> <object class="IBClassDescriptionSource" key="sourceIdentifier"> <string key="majorKey">IBProjectSource</string> @@ -865,9 +1094,17 @@ </object> <int key="IBDocument.localizationMode">0</int> <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaTouchFramework</string> + <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencyDefaults"> + <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS</string> + <real value="1552" key="NS.object.0"/> + </object> + <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies"> + <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3</string> + <integer value="4600" key="NS.object.0"/> + </object> <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool> <int key="IBDocument.defaultPropertyAccessControl">3</int> <bool key="IBDocument.UseAutolayout">YES</bool> - <string key="IBCocoaTouchPluginVersion">2083</string> + <string key="IBCocoaTouchPluginVersion">3682.6</string> </data> </archive> diff --git a/example/mac/macExample/macExample.xcodeproj/project.pbxproj b/example/mac/macExample/macExample.xcodeproj/project.pbxproj index 4cfe53f6..c03d6be7 100644 --- a/example/mac/macExample/macExample.xcodeproj/project.pbxproj +++ b/example/mac/macExample/macExample.xcodeproj/project.pbxproj @@ -19,6 +19,14 @@ C6D42BE516ABB511002BB4F9 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6D42BE416ABB511002BB4F9 /* WebKit.framework */; }; C6D42BE916ACF711002BB4F9 /* MCTMsgListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D42BE816ACF711002BB4F9 /* MCTMsgListViewController.m */; }; C6D42BEC16ACFE3F002BB4F9 /* MCTMsgViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D42BEB16ACFE3F002BB4F9 /* MCTMsgViewController.m */; }; + C6D718CC178B7160008ED15F /* MailCore.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = C6BD28B3170BDFE500A91AC1 /* MailCore.framework */; }; + C6D71912178B7942008ED15F /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6D71911178B7942008ED15F /* SystemConfiguration.framework */; }; + C6D7192F178B7D65008ED15F /* GTMOAuth2Authentication.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D71924178B7D55008ED15F /* GTMOAuth2Authentication.m */; }; + C6D71930178B7D67008ED15F /* GTMOAuth2SignIn.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D71926178B7D55008ED15F /* GTMOAuth2SignIn.m */; }; + C6D71931178B7D6C008ED15F /* GTMHTTPFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D7191F178B7D55008ED15F /* GTMHTTPFetcher.m */; }; + C6D71932178B7D6E008ED15F /* GTMHTTPFetchHistory.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D71921178B7D55008ED15F /* GTMHTTPFetchHistory.m */; }; + C6D71933178B7D72008ED15F /* GTMOAuth2Window.xib in Resources */ = {isa = PBXBuildFile; fileRef = C6D71928178B7D55008ED15F /* GTMOAuth2Window.xib */; }; + C6D71934178B7D72008ED15F /* GTMOAuth2WindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D7192A178B7D55008ED15F /* GTMOAuth2WindowController.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -66,6 +74,19 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + C6D718CB178B714A008ED15F /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + C6D718CC178B7160008ED15F /* MailCore.framework in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ AB666C5717484E6200545290 /* FXKeychain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FXKeychain.h; path = ../../../common/FXKeychain.h; sourceTree = "<group>"; }; AB666C5817484E6200545290 /* FXKeychain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FXKeychain.m; path = ../../../common/FXKeychain.m; sourceTree = "<group>"; }; @@ -90,6 +111,18 @@ C6D42BE816ACF711002BB4F9 /* MCTMsgListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MCTMsgListViewController.m; sourceTree = "<group>"; }; C6D42BEA16ACFE3F002BB4F9 /* MCTMsgViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MCTMsgViewController.h; sourceTree = "<group>"; }; C6D42BEB16ACFE3F002BB4F9 /* MCTMsgViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MCTMsgViewController.m; sourceTree = "<group>"; }; + C6D71911178B7942008ED15F /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + C6D7191E178B7D55008ED15F /* GTMHTTPFetcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMHTTPFetcher.h; sourceTree = "<group>"; }; + C6D7191F178B7D55008ED15F /* GTMHTTPFetcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMHTTPFetcher.m; sourceTree = "<group>"; }; + C6D71920178B7D55008ED15F /* GTMHTTPFetchHistory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMHTTPFetchHistory.h; sourceTree = "<group>"; }; + C6D71921178B7D55008ED15F /* GTMHTTPFetchHistory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMHTTPFetchHistory.m; sourceTree = "<group>"; }; + C6D71923178B7D55008ED15F /* GTMOAuth2Authentication.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2Authentication.h; sourceTree = "<group>"; }; + C6D71924178B7D55008ED15F /* GTMOAuth2Authentication.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2Authentication.m; sourceTree = "<group>"; }; + C6D71925178B7D55008ED15F /* GTMOAuth2SignIn.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2SignIn.h; sourceTree = "<group>"; }; + C6D71926178B7D55008ED15F /* GTMOAuth2SignIn.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2SignIn.m; sourceTree = "<group>"; }; + C6D71928178B7D55008ED15F /* GTMOAuth2Window.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = GTMOAuth2Window.xib; sourceTree = "<group>"; }; + C6D71929178B7D55008ED15F /* GTMOAuth2WindowController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMOAuth2WindowController.h; sourceTree = "<group>"; }; + C6D7192A178B7D55008ED15F /* GTMOAuth2WindowController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMOAuth2WindowController.m; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -97,6 +130,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C6D71912178B7942008ED15F /* SystemConfiguration.framework in Frameworks */, AB666C5B1748558000545290 /* Security.framework in Frameworks */, C6BD28B4170BDFE500A91AC1 /* MailCore.framework in Frameworks */, C6D42BE516ABB511002BB4F9 /* WebKit.framework in Frameworks */, @@ -186,6 +220,7 @@ C6D42BB416ABB39A002BB4F9 /* Frameworks */ = { isa = PBXGroup; children = ( + C6D71911178B7942008ED15F /* SystemConfiguration.framework */, AB666C5A1748558000545290 /* Security.framework */, C6D42BE416ABB511002BB4F9 /* WebKit.framework */, C6D42BB516ABB39A002BB4F9 /* Cocoa.framework */, @@ -207,6 +242,7 @@ C6D42BBB16ABB39A002BB4F9 /* macExample */ = { isa = PBXGroup; children = ( + C6D7191C178B7D55008ED15F /* gtm-oauth2 */, AB666C5217484E3100545290 /* External */, C6D42BBC16ABB39A002BB4F9 /* Supporting Files */, C6D42BC716ABB39A002BB4F9 /* AppDelegate.h */, @@ -229,6 +265,49 @@ name = "Supporting Files"; sourceTree = "<group>"; }; + C6D7191C178B7D55008ED15F /* gtm-oauth2 */ = { + isa = PBXGroup; + children = ( + C6D7191D178B7D55008ED15F /* HTTPFetcher */, + C6D71922178B7D55008ED15F /* Source */, + ); + name = "gtm-oauth2"; + path = "../../../common/gtm-oauth2"; + sourceTree = "<group>"; + }; + C6D7191D178B7D55008ED15F /* HTTPFetcher */ = { + isa = PBXGroup; + children = ( + C6D7191E178B7D55008ED15F /* GTMHTTPFetcher.h */, + C6D7191F178B7D55008ED15F /* GTMHTTPFetcher.m */, + C6D71920178B7D55008ED15F /* GTMHTTPFetchHistory.h */, + C6D71921178B7D55008ED15F /* GTMHTTPFetchHistory.m */, + ); + path = HTTPFetcher; + sourceTree = "<group>"; + }; + C6D71922178B7D55008ED15F /* Source */ = { + isa = PBXGroup; + children = ( + C6D71923178B7D55008ED15F /* GTMOAuth2Authentication.h */, + C6D71924178B7D55008ED15F /* GTMOAuth2Authentication.m */, + C6D71925178B7D55008ED15F /* GTMOAuth2SignIn.h */, + C6D71926178B7D55008ED15F /* GTMOAuth2SignIn.m */, + C6D71927178B7D55008ED15F /* Mac */, + ); + path = Source; + sourceTree = "<group>"; + }; + C6D71927178B7D55008ED15F /* Mac */ = { + isa = PBXGroup; + children = ( + C6D71928178B7D55008ED15F /* GTMOAuth2Window.xib */, + C6D71929178B7D55008ED15F /* GTMOAuth2WindowController.h */, + C6D7192A178B7D55008ED15F /* GTMOAuth2WindowController.m */, + ); + path = Mac; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -239,6 +318,7 @@ C6D42BAD16ABB39A002BB4F9 /* Sources */, C6D42BAE16ABB39A002BB4F9 /* Frameworks */, C6D42BAF16ABB39A002BB4F9 /* Resources */, + C6D718CB178B714A008ED15F /* CopyFiles */, ); buildRules = ( ); @@ -326,6 +406,7 @@ buildActionMask = 2147483647; files = ( C6D42BCC16ABB39A002BB4F9 /* MainMenu.xib in Resources */, + C6D71933178B7D72008ED15F /* GTMOAuth2Window.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -337,12 +418,17 @@ buildActionMask = 2147483647; files = ( C6D42BC916ABB39A002BB4F9 /* AppDelegate.m in Sources */, + C6D71930178B7D67008ED15F /* GTMOAuth2SignIn.m in Sources */, C6D42BE916ACF711002BB4F9 /* MCTMsgListViewController.m in Sources */, C6D42BEC16ACFE3F002BB4F9 /* MCTMsgViewController.m in Sources */, + C6D71931178B7D6C008ED15F /* GTMHTTPFetcher.m in Sources */, + C6D71934178B7D72008ED15F /* GTMOAuth2WindowController.m in Sources */, C64FF38416AF97F400F8C162 /* main.mm in Sources */, C6BD2873170BC5C500A91AC1 /* MCOCIDURLProtocol.mm in Sources */, C6BD2874170BC5C500A91AC1 /* MCOMessageView.mm in Sources */, AB666C5917484E6200545290 /* FXKeychain.m in Sources */, + C6D7192F178B7D65008ED15F /* GTMOAuth2Authentication.m in Sources */, + C6D71932178B7D6E008ED15F /* GTMHTTPFetchHistory.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/example/mac/macExample/macExample/AppDelegate.m b/example/mac/macExample/macExample/AppDelegate.m index 8ba89c37..8cf8e858 100644 --- a/example/mac/macExample/macExample/AppDelegate.m +++ b/example/mac/macExample/macExample/AppDelegate.m @@ -9,6 +9,8 @@ #import "AppDelegate.h" #import <MailCore/MailCore.h> +#import <GTMOAuth2/GTMOAuth2WindowController.h> +#import <GTMOAuth2/GTMOAuth2SignIn.h> #import "MCTMsgListViewController.h" #import "FXKeychain.h" @@ -17,6 +19,7 @@ @property (nonatomic, copy) NSString *login; @property (nonatomic, copy) NSString *hostname; @property (nonatomic, copy) NSString *password; +@property (nonatomic, copy) NSString *oauth2Token; @property (nonatomic, readonly) BOOL loginEnabled; @property (nonatomic, readonly) BOOL loggingIn; @@ -24,21 +27,89 @@ @property (nonatomic, retain) MCOIMAPOperation *checkOp; @end +#define CLIENT_ID @"the-client-id" +#define CLIENT_SECRET @"the-client-secret" +#define KEYCHAIN_ITEM_NAME @"MailCore OAuth 2.0 Token" + @implementation AppDelegate - (void) dealloc { + self.login = nil; + self.hostname = nil; + self.password = nil; + self.oauth2Token = nil; + self.session = nil; + self.checkOp = nil; [super dealloc]; } - (void) awakeFromNib { [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"Hostname": @"imap.gmail.com" }]; - - self.login = [[NSUserDefaults standardUserDefaults] stringForKey:@"Login"]; self.hostname = [[NSUserDefaults standardUserDefaults] stringForKey:@"Hostname"]; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"OAuth2Enabled"]) { + [self startOAuth2]; + } + else { + [self startLogin]; + } +} + +- (void) startOAuth2 +{ + GTMOAuth2Authentication * auth = [GTMOAuth2WindowController authForGoogleFromKeychainForName:KEYCHAIN_ITEM_NAME + clientID:CLIENT_ID + clientSecret:CLIENT_SECRET]; + + if ([auth refreshToken] == nil) { + GTMOAuth2WindowController *windowController = [[[GTMOAuth2WindowController alloc] initWithScope:@"https://mail.google.com/" + clientID:CLIENT_ID + clientSecret:CLIENT_SECRET + keychainItemName:KEYCHAIN_ITEM_NAME + resourceBundle:[NSBundle bundleForClass:[GTMOAuth2WindowController class]]] autorelease]; + [windowController signInSheetModalForWindow:nil + delegate:self + finishedSelector:@selector(windowController:finishedWithAuth:error:)]; + } + else { + [auth beginTokenFetchWithDelegate:self + didFinishSelector:@selector(auth:finishedRefreshWithFetcher:error:)]; + } +} + +- (void)auth:(GTMOAuth2Authentication *)auth +finishedRefreshWithFetcher:(GTMHTTPFetcher *)fetcher + error:(NSError *)error { + [self windowController:nil finishedWithAuth:auth error:error]; +} + +- (void)windowController:(GTMOAuth2WindowController *)viewController + finishedWithAuth:(GTMOAuth2Authentication *)auth + error:(NSError *)error +{ + if (error != nil) { + NSLog(@"request failed"); + // Authentication failed + } else { + // Authentication succeeded + [self retrieveAccessToken:auth]; + } +} + +- (void) retrieveAccessToken:(GTMOAuth2Authentication *)auth +{ + self.login = [auth userEmail]; + self.oauth2Token = [auth accessToken]; + + [self accountLogin:nil]; +} + +- (void) startLogin +{ + self.login = [[NSUserDefaults standardUserDefaults] stringForKey:@"Login"]; self.password = [[FXKeychain defaultKeychain] objectForKey:@"Password"]; - if (self.login.length && self.password.length) { [self accountLogin:nil]; } else { @@ -48,22 +119,36 @@ - (void) accountLogin:(id)sender { - NSLog(@"try login"); - [[NSUserDefaults standardUserDefaults] setObject:self.login forKey:@"Login"]; - [[NSUserDefaults standardUserDefaults] setObject:self.hostname forKey:@"Hostname"]; - - if (![[[FXKeychain defaultKeychain] objectForKey:@"Password"] isEqualToString:self.password]) { - [[FXKeychain defaultKeychain] removeObjectForKey:@"Password"]; - [[FXKeychain defaultKeychain] setObject:self.password forKey:@"Password"]; - } - - self.session = [[MCOIMAPSession alloc] init]; + self.session = [[[MCOIMAPSession alloc] init] autorelease]; [self.session setHostname:self.hostname]; [self.session setPort:993]; - [self.session setUsername:self.login]; - [self.session setPassword:self.password]; + + NSLog(@"try login"); + if (self.oauth2Token != nil) { + [self.session setUsername:self.login]; + [self.session setOAuth2Token:self.oauth2Token]; + [self.session setAuthType:MCOAuthTypeXOAuth2]; + } + else { + [[NSUserDefaults standardUserDefaults] setObject:self.login forKey:@"Login"]; + [[NSUserDefaults standardUserDefaults] setObject:self.hostname forKey:@"Hostname"]; + + if (![[[FXKeychain defaultKeychain] objectForKey:@"Password"] isEqualToString:self.password]) { + [[FXKeychain defaultKeychain] removeObjectForKey:@"Password"]; + [[FXKeychain defaultKeychain] setObject:self.password forKey:@"Password"]; + } + + [self.session setUsername:self.login]; + [self.session setPassword:self.password]; + } + [self.session setConnectionType:MCOConnectionTypeTLS]; self.checkOp = [self.session checkAccountOperation]; + self.session.connectionLogger = ^(void * connectionID, MCOConnectionLogType type, NSData * data) { + if (type != MCOConnectionLogTypeSentPrivate) { + NSLog(@"event logged:%p %i withData: %@", connectionID, type, [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); + } + }; NSLog(@"start op"); [self.checkOp start:^(NSError * error) { @@ -82,7 +167,7 @@ if (error != nil) [_accountWindow makeKeyAndOrderFront:nil]; - [_msgListViewController connectWithHostname:self.hostname login:self.login password:self.password]; + [_msgListViewController connectWithHostname:self.hostname login:self.login password:self.password oauth2Token:self.oauth2Token]; }]; } diff --git a/example/mac/macExample/macExample/MCTMsgListViewController.h b/example/mac/macExample/macExample/MCTMsgListViewController.h index 2998f1a2..0dee2422 100644 --- a/example/mac/macExample/macExample/MCTMsgListViewController.h +++ b/example/mac/macExample/macExample/MCTMsgListViewController.h @@ -19,6 +19,9 @@ NSArray * _messages; } -- (void) connectWithHostname:(NSString *)hostname login:(NSString *)login password:(NSString *)password; +- (void) connectWithHostname:(NSString *)hostname + login:(NSString *)login + password:(NSString *)password + oauth2Token:(NSString *)oauth2Token; @end diff --git a/example/mac/macExample/macExample/MCTMsgListViewController.m b/example/mac/macExample/macExample/MCTMsgListViewController.m index 498c66d1..0d7cee7a 100644 --- a/example/mac/macExample/macExample/MCTMsgListViewController.m +++ b/example/mac/macExample/macExample/MCTMsgListViewController.m @@ -21,20 +21,36 @@ @implementation MCTMsgListViewController -- (void) connectWithHostname:(NSString *)hostname login:(NSString *)login password:(NSString *)password +- (void) connectWithHostname:(NSString *)hostname + login:(NSString *)login + password:(NSString *)password + oauth2Token:(NSString *)oauth2Token { [_msgViewController setFolder:FOLDER]; - if (([login length] == 0) || ([password length] == 0)) - return; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"OAuth2Enabled"]) { + if (([login length] == 0) || ([oauth2Token length] == 0)) + return; + } + else { + if (([login length] == 0) || ([password length] == 0)) + return; + } self.loading = YES; _session = [[MCOIMAPSession alloc] init]; [_session setHostname:hostname]; [_session setPort:993]; - [_session setUsername:login]; - [_session setPassword:password]; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"OAuth2Enabled"]) { + [_session setUsername:login]; + [_session setOAuth2Token:oauth2Token]; + [_session setAuthType:MCOAuthTypeXOAuth2]; + } + else { + [_session setUsername:login]; + [_session setPassword:password]; + } [_session setConnectionType:MCOConnectionTypeTLS]; MCOIMAPMessagesRequestKind requestKind = (MCOIMAPMessagesRequestKind) @@ -53,6 +69,7 @@ NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"header.date" ascending:NO]; _messages = [[messages sortedArrayUsingDescriptors:@[sort]] retain]; + NSLog(@"error: %@", error); NSLog(@"%i messages", (int) [_messages count]); //NSLog(@"%@", _messages); [_tableView reloadData]; diff --git a/example/mac/macExample/macExample/en.lproj/MainMenu.xib b/example/mac/macExample/macExample/en.lproj/MainMenu.xib index e49bd706..8470cb69 100644 --- a/example/mac/macExample/macExample/en.lproj/MainMenu.xib +++ b/example/mac/macExample/macExample/en.lproj/MainMenu.xib @@ -1,4004 +1,696 @@ -<?xml version="1.0" encoding="UTF-8"?> -<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="8.00"> - <data> - <int key="IBDocument.SystemTarget">1080</int> - <string key="IBDocument.SystemVersion">12D78</string> - <string key="IBDocument.InterfaceBuilderVersion">3084</string> - <string key="IBDocument.AppKitVersion">1187.37</string> - <string key="IBDocument.HIToolboxVersion">626.00</string> - <object class="NSMutableDictionary" key="IBDocument.PluginVersions"> - <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="NS.object.0">3084</string> - </object> - <array key="IBDocument.IntegratedClassDependencies"> - <string>NSButton</string> - <string>NSButtonCell</string> - <string>NSCustomObject</string> - <string>NSCustomView</string> - <string>NSMenu</string> - <string>NSMenuItem</string> - <string>NSProgressIndicator</string> - <string>NSScrollView</string> - <string>NSScroller</string> - <string>NSSecureTextField</string> - <string>NSSecureTextFieldCell</string> - <string>NSTableColumn</string> - <string>NSTableHeaderView</string> - <string>NSTableView</string> - <string>NSTextField</string> - <string>NSTextFieldCell</string> - <string>NSUserDefaultsController</string> - <string>NSView</string> - <string>NSViewController</string> - <string>NSWindowTemplate</string> - </array> - <array key="IBDocument.PluginDependencies"> - <string>com.apple.InterfaceBuilder.CocoaPlugin</string> - </array> - <object class="NSMutableDictionary" key="IBDocument.Metadata"> - <string key="NS.key.0">PluginDependencyRecalculationVersion</string> - <integer value="1" key="NS.object.0"/> - </object> - <array class="NSMutableArray" key="IBDocument.RootObjects" id="1048"> - <object class="NSCustomObject" id="1021"> - <string key="NSClassName">NSApplication</string> - </object> - <object class="NSCustomObject" id="1014"> - <string key="NSClassName">FirstResponder</string> - </object> - <object class="NSCustomObject" id="1050"> - <string key="NSClassName">NSApplication</string> - </object> - <object class="NSMenu" id="649796088"> - <string key="NSTitle">AMainMenu</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="694149608"> - <reference key="NSMenu" ref="649796088"/> - <string key="NSTitle">macExample</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <object class="NSCustomResource" key="NSOnImage" id="35465992"> - <string key="NSClassName">NSImage</string> - <string key="NSResourceName">NSMenuCheckmark</string> - </object> - <object class="NSCustomResource" key="NSMixedImage" id="502551668"> - <string key="NSClassName">NSImage</string> - <string key="NSResourceName">NSMenuMixedState</string> - </object> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="110575045"> - <string key="NSTitle">macExample</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="238522557"> - <reference key="NSMenu" ref="110575045"/> - <string key="NSTitle">About macExample</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="304266470"> - <reference key="NSMenu" ref="110575045"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="609285721"> - <reference key="NSMenu" ref="110575045"/> - <string key="NSTitle">Preferences…</string> - <string key="NSKeyEquiv">,</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="481834944"> - <reference key="NSMenu" ref="110575045"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="1046388886"> - <reference key="NSMenu" ref="110575045"/> - <string key="NSTitle">Services</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="752062318"> - <string key="NSTitle">Services</string> - <array class="NSMutableArray" key="NSMenuItems"/> - <string key="NSName">_NSServicesMenu</string> - </object> - </object> - <object class="NSMenuItem" id="646227648"> - <reference key="NSMenu" ref="110575045"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="755159360"> - <reference key="NSMenu" ref="110575045"/> - <string key="NSTitle">Hide macExample</string> - <string key="NSKeyEquiv">h</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="342932134"> - <reference key="NSMenu" ref="110575045"/> - <string key="NSTitle">Hide Others</string> - <string key="NSKeyEquiv">h</string> - <int key="NSKeyEquivModMask">1572864</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="908899353"> - <reference key="NSMenu" ref="110575045"/> - <string key="NSTitle">Show All</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="1056857174"> - <reference key="NSMenu" ref="110575045"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="632727374"> - <reference key="NSMenu" ref="110575045"/> - <string key="NSTitle">Quit testUI</string> - <string key="NSKeyEquiv">q</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - <string key="NSName">_NSAppleMenu</string> - </object> - </object> - <object class="NSMenuItem" id="379814623"> - <reference key="NSMenu" ref="649796088"/> - <string key="NSTitle">File</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="720053764"> - <string key="NSTitle">File</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="354256217"> - <reference key="NSMenu" ref="720053764"/> - <string key="NSTitle">Account...</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="521030078"> - <reference key="NSMenu" ref="720053764"/> - <string key="NSTitle">Fetch Full Message</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="425164168"> - <reference key="NSMenu" ref="720053764"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="776162233"> - <reference key="NSMenu" ref="720053764"/> - <string key="NSTitle">Close</string> - <string key="NSKeyEquiv">w</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - </object> - </object> - <object class="NSMenuItem" id="952259628"> - <reference key="NSMenu" ref="649796088"/> - <string key="NSTitle">Edit</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="789758025"> - <string key="NSTitle">Edit</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="1058277027"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Undo</string> - <string key="NSKeyEquiv">z</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="790794224"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Redo</string> - <string key="NSKeyEquiv">Z</string> - <int key="NSKeyEquivModMask">1179648</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="1040322652"> - <reference key="NSMenu" ref="789758025"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="296257095"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Cut</string> - <string key="NSKeyEquiv">x</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="860595796"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Copy</string> - <string key="NSKeyEquiv">c</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="29853731"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Paste</string> - <string key="NSKeyEquiv">v</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="82994268"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Paste and Match Style</string> - <string key="NSKeyEquiv">V</string> - <int key="NSKeyEquivModMask">1572864</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="437104165"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Delete</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="583158037"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Select All</string> - <string key="NSKeyEquiv">a</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="212016141"> - <reference key="NSMenu" ref="789758025"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="892235320"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Find</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="963351320"> - <string key="NSTitle">Find</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="447796847"> - <reference key="NSMenu" ref="963351320"/> - <string key="NSTitle">Find…</string> - <string key="NSKeyEquiv">f</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">1</int> - </object> - <object class="NSMenuItem" id="738670835"> - <reference key="NSMenu" ref="963351320"/> - <string key="NSTitle">Find and Replace…</string> - <string key="NSKeyEquiv">f</string> - <int key="NSKeyEquivModMask">1572864</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">12</int> - </object> - <object class="NSMenuItem" id="326711663"> - <reference key="NSMenu" ref="963351320"/> - <string key="NSTitle">Find Next</string> - <string key="NSKeyEquiv">g</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">2</int> - </object> - <object class="NSMenuItem" id="270902937"> - <reference key="NSMenu" ref="963351320"/> - <string key="NSTitle">Find Previous</string> - <string key="NSKeyEquiv">G</string> - <int key="NSKeyEquivModMask">1179648</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">3</int> - </object> - <object class="NSMenuItem" id="159080638"> - <reference key="NSMenu" ref="963351320"/> - <string key="NSTitle">Use Selection for Find</string> - <string key="NSKeyEquiv">e</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">7</int> - </object> - <object class="NSMenuItem" id="88285865"> - <reference key="NSMenu" ref="963351320"/> - <string key="NSTitle">Jump to Selection</string> - <string key="NSKeyEquiv">j</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - </object> - </object> - <object class="NSMenuItem" id="972420730"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Spelling and Grammar</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="769623530"> - <string key="NSTitle">Spelling and Grammar</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="679648819"> - <reference key="NSMenu" ref="769623530"/> - <string key="NSTitle">Show Spelling and Grammar</string> - <string key="NSKeyEquiv">:</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="96193923"> - <reference key="NSMenu" ref="769623530"/> - <string key="NSTitle">Check Document Now</string> - <string key="NSKeyEquiv">;</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="859480356"> - <reference key="NSMenu" ref="769623530"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="948374510"> - <reference key="NSMenu" ref="769623530"/> - <string key="NSTitle">Check Spelling While Typing</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="967646866"> - <reference key="NSMenu" ref="769623530"/> - <string key="NSTitle">Check Grammar With Spelling</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="795346622"> - <reference key="NSMenu" ref="769623530"/> - <string key="NSTitle">Correct Spelling Automatically</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - </object> - </object> - <object class="NSMenuItem" id="507821607"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Substitutions</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="698887838"> - <string key="NSTitle">Substitutions</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="65139061"> - <reference key="NSMenu" ref="698887838"/> - <string key="NSTitle">Show Substitutions</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="19036812"> - <reference key="NSMenu" ref="698887838"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="605118523"> - <reference key="NSMenu" ref="698887838"/> - <string key="NSTitle">Smart Copy/Paste</string> - <string key="NSKeyEquiv">f</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">1</int> - </object> - <object class="NSMenuItem" id="197661976"> - <reference key="NSMenu" ref="698887838"/> - <string key="NSTitle">Smart Quotes</string> - <string key="NSKeyEquiv">g</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">2</int> - </object> - <object class="NSMenuItem" id="672708820"> - <reference key="NSMenu" ref="698887838"/> - <string key="NSTitle">Smart Dashes</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="708854459"> - <reference key="NSMenu" ref="698887838"/> - <string key="NSTitle">Smart Links</string> - <string key="NSKeyEquiv">G</string> - <int key="NSKeyEquivModMask">1179648</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">3</int> - </object> - <object class="NSMenuItem" id="537092702"> - <reference key="NSMenu" ref="698887838"/> - <string key="NSTitle">Text Replacement</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - </object> - </object> - <object class="NSMenuItem" id="288088188"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Transformations</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="579392910"> - <string key="NSTitle">Transformations</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="1060694897"> - <reference key="NSMenu" ref="579392910"/> - <string key="NSTitle">Make Upper Case</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="879586729"> - <reference key="NSMenu" ref="579392910"/> - <string key="NSTitle">Make Lower Case</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="56570060"> - <reference key="NSMenu" ref="579392910"/> - <string key="NSTitle">Capitalize</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - </object> - </object> - <object class="NSMenuItem" id="676164635"> - <reference key="NSMenu" ref="789758025"/> - <string key="NSTitle">Speech</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="785027613"> - <string key="NSTitle">Speech</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="731782645"> - <reference key="NSMenu" ref="785027613"/> - <string key="NSTitle">Start Speaking</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="680220178"> - <reference key="NSMenu" ref="785027613"/> - <string key="NSTitle">Stop Speaking</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - </object> - </object> - </array> - </object> - </object> - <object class="NSMenuItem" id="302598603"> - <reference key="NSMenu" ref="649796088"/> - <string key="NSTitle">Format</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="941447902"> - <string key="NSTitle">Format</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="792887677"> - <reference key="NSMenu" ref="941447902"/> - <string key="NSTitle">Font</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="786677654"> - <string key="NSTitle">Font</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="159677712"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Show Fonts</string> - <string key="NSKeyEquiv">t</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="305399458"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Bold</string> - <string key="NSKeyEquiv">b</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">2</int> - </object> - <object class="NSMenuItem" id="814362025"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Italic</string> - <string key="NSKeyEquiv">i</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">1</int> - </object> - <object class="NSMenuItem" id="330926929"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Underline</string> - <string key="NSKeyEquiv">u</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="533507878"> - <reference key="NSMenu" ref="786677654"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="158063935"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Bigger</string> - <string key="NSKeyEquiv">+</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">3</int> - </object> - <object class="NSMenuItem" id="885547335"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Smaller</string> - <string key="NSKeyEquiv">-</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <int key="NSTag">4</int> - </object> - <object class="NSMenuItem" id="901062459"> - <reference key="NSMenu" ref="786677654"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="767671776"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Kern</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="175441468"> - <string key="NSTitle">Kern</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="252969304"> - <reference key="NSMenu" ref="175441468"/> - <string key="NSTitle">Use Default</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="766922938"> - <reference key="NSMenu" ref="175441468"/> - <string key="NSTitle">Use None</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="677519740"> - <reference key="NSMenu" ref="175441468"/> - <string key="NSTitle">Tighten</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="238351151"> - <reference key="NSMenu" ref="175441468"/> - <string key="NSTitle">Loosen</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - </object> - </object> - <object class="NSMenuItem" id="691570813"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Ligatures</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="1058217995"> - <string key="NSTitle">Ligatures</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="706297211"> - <reference key="NSMenu" ref="1058217995"/> - <string key="NSTitle">Use Default</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="568384683"> - <reference key="NSMenu" ref="1058217995"/> - <string key="NSTitle">Use None</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="663508465"> - <reference key="NSMenu" ref="1058217995"/> - <string key="NSTitle">Use All</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - </object> - </object> - <object class="NSMenuItem" id="769124883"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Baseline</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="18263474"> - <string key="NSTitle">Baseline</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="257962622"> - <reference key="NSMenu" ref="18263474"/> - <string key="NSTitle">Use Default</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="644725453"> - <reference key="NSMenu" ref="18263474"/> - <string key="NSTitle">Superscript</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="1037576581"> - <reference key="NSMenu" ref="18263474"/> - <string key="NSTitle">Subscript</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="941806246"> - <reference key="NSMenu" ref="18263474"/> - <string key="NSTitle">Raise</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="1045724900"> - <reference key="NSMenu" ref="18263474"/> - <string key="NSTitle">Lower</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - </object> - </object> - <object class="NSMenuItem" id="739652853"> - <reference key="NSMenu" ref="786677654"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="1012600125"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Show Colors</string> - <string key="NSKeyEquiv">C</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="214559597"> - <reference key="NSMenu" ref="786677654"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="596732606"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Copy Style</string> - <string key="NSKeyEquiv">c</string> - <int key="NSKeyEquivModMask">1572864</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="393423671"> - <reference key="NSMenu" ref="786677654"/> - <string key="NSTitle">Paste Style</string> - <string key="NSKeyEquiv">v</string> - <int key="NSKeyEquivModMask">1572864</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - <string key="NSName">_NSFontMenu</string> - </object> - </object> - <object class="NSMenuItem" id="215659978"> - <reference key="NSMenu" ref="941447902"/> - <string key="NSTitle">Text</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="446991534"> - <string key="NSTitle">Text</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="875092757"> - <reference key="NSMenu" ref="446991534"/> - <string key="NSTitle">Align Left</string> - <string key="NSKeyEquiv">{</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="630155264"> - <reference key="NSMenu" ref="446991534"/> - <string key="NSTitle">Center</string> - <string key="NSKeyEquiv">|</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="945678886"> - <reference key="NSMenu" ref="446991534"/> - <string key="NSTitle">Justify</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="512868991"> - <reference key="NSMenu" ref="446991534"/> - <string key="NSTitle">Align Right</string> - <string key="NSKeyEquiv">}</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="163117631"> - <reference key="NSMenu" ref="446991534"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="31516759"> - <reference key="NSMenu" ref="446991534"/> - <string key="NSTitle">Writing Direction</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="956096989"> - <string key="NSTitle">Writing Direction</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="257099033"> - <reference key="NSMenu" ref="956096989"/> - <bool key="NSIsDisabled">YES</bool> - <string key="NSTitle">Paragraph</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="551969625"> - <reference key="NSMenu" ref="956096989"/> - <string type="base64-UTF8" key="NSTitle">CURlZmF1bHQ</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="249532473"> - <reference key="NSMenu" ref="956096989"/> - <string type="base64-UTF8" key="NSTitle">CUxlZnQgdG8gUmlnaHQ</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="607364498"> - <reference key="NSMenu" ref="956096989"/> - <string type="base64-UTF8" key="NSTitle">CVJpZ2h0IHRvIExlZnQ</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="508151438"> - <reference key="NSMenu" ref="956096989"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="981751889"> - <reference key="NSMenu" ref="956096989"/> - <bool key="NSIsDisabled">YES</bool> - <string key="NSTitle">Selection</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="380031999"> - <reference key="NSMenu" ref="956096989"/> - <string type="base64-UTF8" key="NSTitle">CURlZmF1bHQ</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="825984362"> - <reference key="NSMenu" ref="956096989"/> - <string type="base64-UTF8" key="NSTitle">CUxlZnQgdG8gUmlnaHQ</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="560145579"> - <reference key="NSMenu" ref="956096989"/> - <string type="base64-UTF8" key="NSTitle">CVJpZ2h0IHRvIExlZnQ</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - </object> - </object> - <object class="NSMenuItem" id="908105787"> - <reference key="NSMenu" ref="446991534"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="644046920"> - <reference key="NSMenu" ref="446991534"/> - <string key="NSTitle">Show Ruler</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="231811626"> - <reference key="NSMenu" ref="446991534"/> - <string key="NSTitle">Copy Ruler</string> - <string key="NSKeyEquiv">c</string> - <int key="NSKeyEquivModMask">1310720</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="883618387"> - <reference key="NSMenu" ref="446991534"/> - <string key="NSTitle">Paste Ruler</string> - <string key="NSKeyEquiv">v</string> - <int key="NSKeyEquivModMask">1310720</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - </object> - </object> - </array> - </object> - </object> - <object class="NSMenuItem" id="713487014"> - <reference key="NSMenu" ref="649796088"/> - <string key="NSTitle">Window</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="835318025"> - <string key="NSTitle">Window</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="1011231497"> - <reference key="NSMenu" ref="835318025"/> - <string key="NSTitle">Minimize</string> - <string key="NSKeyEquiv">m</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="575023229"> - <reference key="NSMenu" ref="835318025"/> - <string key="NSTitle">Zoom</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="299356726"> - <reference key="NSMenu" ref="835318025"/> - <bool key="NSIsDisabled">YES</bool> - <bool key="NSIsSeparator">YES</bool> - <string key="NSTitle"/> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - <object class="NSMenuItem" id="625202149"> - <reference key="NSMenu" ref="835318025"/> - <string key="NSTitle">Bring All to Front</string> - <string key="NSKeyEquiv"/> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - <string key="NSName">_NSWindowsMenu</string> - </object> - </object> - <object class="NSMenuItem" id="448692316"> - <reference key="NSMenu" ref="649796088"/> - <string key="NSTitle">Help</string> - <string key="NSKeyEquiv"/> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - <string key="NSAction">submenuAction:</string> - <object class="NSMenu" key="NSSubmenu" id="992780483"> - <string key="NSTitle">Help</string> - <array class="NSMutableArray" key="NSMenuItems"> - <object class="NSMenuItem" id="105068016"> - <reference key="NSMenu" ref="992780483"/> - <string key="NSTitle">macExample Help</string> - <string key="NSKeyEquiv">?</string> - <int key="NSKeyEquivModMask">1048576</int> - <int key="NSMnemonicLoc">2147483647</int> - <reference key="NSOnImage" ref="35465992"/> - <reference key="NSMixedImage" ref="502551668"/> - </object> - </array> - <string key="NSName">_NSHelpMenu</string> - </object> - </object> - </array> - <string key="NSName">_NSMainMenu</string> - </object> - <object class="NSWindowTemplate" id="972006081"> - <int key="NSWindowStyleMask">15</int> - <int key="NSWindowBacking">2</int> - <string key="NSWindowRect">{{344, 127}, {768, 493}}</string> - <int key="NSWTFlags">1948778496</int> - <string key="NSWindowTitle">testUI</string> - <string key="NSWindowClass">NSWindow</string> - <nil key="NSViewClass"/> - <nil key="NSUserInterfaceItemIdentifier"/> - <object class="NSView" key="NSWindowView" id="439893737"> - <reference key="NSNextResponder"/> - <int key="NSvFlags">256</int> - <array class="NSMutableArray" key="NSSubviews"> - <object class="NSScrollView" id="665197104"> - <reference key="NSNextResponder" ref="439893737"/> - <int key="NSvFlags">284</int> - <array class="NSMutableArray" key="NSSubviews"> - <object class="NSClipView" id="1057040196"> - <reference key="NSNextResponder" ref="665197104"/> - <int key="NSvFlags">2304</int> - <array class="NSMutableArray" key="NSSubviews"> - <object class="NSTableView" id="818403152"> - <reference key="NSNextResponder" ref="1057040196"/> - <int key="NSvFlags">256</int> - <string key="NSFrameSize">{192, 477}</string> - <reference key="NSSuperview" ref="1057040196"/> - <reference key="NSWindow"/> - <reference key="NSNextKeyView" ref="849854472"/> - <string key="NSReuseIdentifierKey">_NS:13</string> - <bool key="NSEnabled">YES</bool> - <bool key="NSAllowsLogicalLayoutDirection">NO</bool> - <bool key="NSControlAllowsExpansionToolTips">YES</bool> - <object class="NSTableHeaderView" key="NSHeaderView" id="8853202"> - <reference key="NSNextResponder" ref="958887582"/> - <int key="NSvFlags">256</int> - <string key="NSFrameSize">{192, 17}</string> - <reference key="NSSuperview" ref="958887582"/> - <reference key="NSWindow"/> - <reference key="NSNextKeyView" ref="1057040196"/> - <string key="NSReuseIdentifierKey">_NS:16</string> - <reference key="NSTableView" ref="818403152"/> - </object> - <object class="_NSCornerView" key="NSCornerView"> - <nil key="NSNextResponder"/> - <int key="NSvFlags">-2147483392</int> - <string key="NSFrame">{{224, 0}, {16, 17}}</string> - <reference key="NSNextKeyView" ref="1057040196"/> - <string key="NSReuseIdentifierKey">_NS:19</string> - </object> - <array class="NSMutableArray" key="NSTableColumns"> - <object class="NSTableColumn" id="631488934"> - <double key="NSWidth">188.68359375</double> - <double key="NSMinWidth">40</double> - <double key="NSMaxWidth">1000</double> - <object class="NSTableHeaderCell" key="NSHeaderCell"> - <int key="NSCellFlags">75497536</int> - <int key="NSCellFlags2">2048</int> - <string key="NSContents"/> - <object class="NSFont" key="NSSupport"> - <string key="NSName">LucidaGrande</string> - <double key="NSSize">11</double> - <int key="NSfFlags">3100</int> - </object> - <object class="NSColor" key="NSBackgroundColor"> - <int key="NSColorSpace">3</int> - <bytes key="NSWhite">MC4zMzMzMzI5ODU2AA</bytes> - </object> - <object class="NSColor" key="NSTextColor"> - <int key="NSColorSpace">6</int> - <string key="NSCatalogName">System</string> - <string key="NSColorName">headerTextColor</string> - <object class="NSColor" key="NSColor" id="342765170"> - <int key="NSColorSpace">3</int> - <bytes key="NSWhite">MAA</bytes> - </object> - </object> - </object> - <object class="NSTextFieldCell" key="NSDataCell" id="665052716"> - <int key="NSCellFlags">337641536</int> - <int key="NSCellFlags2">2048</int> - <string key="NSContents">Text Cell</string> - <object class="NSFont" key="NSSupport" id="640234263"> - <string key="NSName">LucidaGrande</string> - <double key="NSSize">13</double> - <int key="NSfFlags">1044</int> - </object> - <reference key="NSControlView" ref="818403152"/> - <object class="NSColor" key="NSBackgroundColor" id="866538965"> - <int key="NSColorSpace">6</int> - <string key="NSCatalogName">System</string> - <string key="NSColorName">controlBackgroundColor</string> - <object class="NSColor" key="NSColor" id="428798902"> - <int key="NSColorSpace">3</int> - <bytes key="NSWhite">MC42NjY2NjY2NjY3AA</bytes> - </object> - </object> - <object class="NSColor" key="NSTextColor" id="567109269"> - <int key="NSColorSpace">6</int> - <string key="NSCatalogName">System</string> - <string key="NSColorName">controlTextColor</string> - <reference key="NSColor" ref="342765170"/> - </object> - </object> - <int key="NSResizingMask">3</int> - <bool key="NSIsResizeable">YES</bool> - <bool key="NSIsEditable">YES</bool> - <reference key="NSTableView" ref="818403152"/> - </object> - </array> - <double key="NSIntercellSpacingWidth">3</double> - <double key="NSIntercellSpacingHeight">2</double> - <object class="NSColor" key="NSBackgroundColor" id="492204717"> - <int key="NSColorSpace">3</int> - <bytes key="NSWhite">MQA</bytes> - </object> - <object class="NSColor" key="NSGridColor"> - <int key="NSColorSpace">6</int> - <string key="NSCatalogName">System</string> - <string key="NSColorName">gridColor</string> - <object class="NSColor" key="NSColor"> - <int key="NSColorSpace">3</int> - <bytes key="NSWhite">MC41AA</bytes> - </object> - </object> - <double key="NSRowHeight">17</double> - <int key="NSTvFlags">-700448768</int> - <reference key="NSDelegate"/> - <reference key="NSDataSource"/> - <int key="NSColumnAutoresizingStyle">4</int> - <int key="NSDraggingSourceMaskForLocal">15</int> - <int key="NSDraggingSourceMaskForNonLocal">0</int> - <bool key="NSAllowsTypeSelect">YES</bool> - <int key="NSTableViewDraggingDestinationStyle">0</int> - <int key="NSTableViewGroupRowStyle">1</int> - </object> - </array> - <string key="NSFrame">{{1, 17}, {192, 477}}</string> - <reference key="NSSuperview" ref="665197104"/> - <reference key="NSWindow"/> - <reference key="NSNextKeyView" ref="818403152"/> - <string key="NSReuseIdentifierKey">_NS:11</string> - <reference key="NSDocView" ref="818403152"/> - <reference key="NSBGColor" ref="866538965"/> - <int key="NScvFlags">4</int> - </object> - <object class="NSScroller" id="849854472"> - <reference key="NSNextResponder" ref="665197104"/> - <int key="NSvFlags">-2147483392</int> - <string key="NSFrame">{{224, 17}, {15, 102}}</string> - <reference key="NSSuperview" ref="665197104"/> - <reference key="NSWindow"/> - <reference key="NSNextKeyView" ref="1027823325"/> - <string key="NSReuseIdentifierKey">_NS:58</string> - <bool key="NSAllowsLogicalLayoutDirection">NO</bool> - <reference key="NSTarget" ref="665197104"/> - <string key="NSAction">_doScroller:</string> - <double key="NSCurValue">37</double> - <double key="NSPercent">0.1947367936372757</double> - </object> - <object class="NSScroller" id="1027823325"> - <reference key="NSNextResponder" ref="665197104"/> - <int key="NSvFlags">-2147483392</int> - <string key="NSFrame">{{1, 478}, {335, 16}}</string> - <reference key="NSSuperview" ref="665197104"/> - <reference key="NSWindow"/> - <reference key="NSNextKeyView" ref="220665056"/> - <string key="NSReuseIdentifierKey">_NS:60</string> - <bool key="NSAllowsLogicalLayoutDirection">NO</bool> - <int key="NSsFlags">1</int> - <reference key="NSTarget" ref="665197104"/> - <string key="NSAction">_doScroller:</string> - <double key="NSCurValue">1</double> - <double key="NSPercent">0.97953216374269003</double> - </object> - <object class="NSClipView" id="958887582"> - <reference key="NSNextResponder" ref="665197104"/> - <int key="NSvFlags">2304</int> - <array class="NSMutableArray" key="NSSubviews"> - <reference ref="8853202"/> - </array> - <string key="NSFrame">{{1, 0}, {192, 17}}</string> - <reference key="NSSuperview" ref="665197104"/> - <reference key="NSWindow"/> - <reference key="NSNextKeyView" ref="8853202"/> - <string key="NSReuseIdentifierKey">_NS:15</string> - <reference key="NSDocView" ref="8853202"/> - <reference key="NSBGColor" ref="866538965"/> - <int key="NScvFlags">4</int> - </object> - </array> - <string key="NSFrame">{{-1, 0}, {194, 495}}</string> - <reference key="NSSuperview" ref="439893737"/> - <reference key="NSWindow"/> - <reference key="NSNextKeyView" ref="958887582"/> - <string key="NSReuseIdentifierKey">_NS:9</string> - <int key="NSsFlags">133682</int> - <reference key="NSVScroller" ref="849854472"/> - <reference key="NSHScroller" ref="1027823325"/> - <reference key="NSContentView" ref="1057040196"/> - <reference key="NSHeaderClipView" ref="958887582"/> - <bytes key="NSScrollAmts">QSAAAEEgAABBmAAAQZgAAA</bytes> - <double key="NSMinMagnification">0.25</double> - <double key="NSMaxMagnification">4</double> - <double key="NSMagnification">1</double> - </object> - <object class="NSCustomView" id="220665056"> - <reference key="NSNextResponder" ref="439893737"/> - <int key="NSvFlags">274</int> - <array class="NSMutableArray" key="NSSubviews"/> - <string key="NSFrame">{{201, 0}, {567, 493}}</string> - <reference key="NSSuperview" ref="439893737"/> - <reference key="NSWindow"/> - <reference key="NSNextKeyView" ref="677395401"/> - <string key="NSReuseIdentifierKey">_NS:9</string> - <string key="NSClassName">MCOMessageView</string> - </object> - <object class="NSProgressIndicator" id="677395401"> - <reference key="NSNextResponder" ref="439893737"/> - <int key="NSvFlags">268</int> - <string key="NSFrame">{{468, 230}, {32, 32}}</string> - <reference key="NSSuperview" ref="439893737"/> - <reference key="NSWindow"/> - <reference key="NSNextKeyView"/> - <string key="NSReuseIdentifierKey">_NS:945</string> - <int key="NSpiFlags">20490</int> - <double key="NSMaxValue">100</double> - </object> - </array> - <string key="NSFrameSize">{768, 493}</string> - <reference key="NSSuperview"/> - <reference key="NSWindow"/> - <reference key="NSNextKeyView" ref="665197104"/> - <bool key="NSViewIsLayerTreeHost">YES</bool> - </object> - <string key="NSScreenRect">{{0, 0}, {1440, 878}}</string> - <string key="NSMaxSize">{10000000000000, 10000000000000}</string> - <bool key="NSWindowIsRestorable">YES</bool> - </object> - <object class="NSWindowTemplate" id="389393842"> - <int key="NSWindowStyleMask">3</int> - <int key="NSWindowBacking">2</int> - <string key="NSWindowRect">{{505, 474}, {409, 153}}</string> - <int key="NSWTFlags">611845120</int> - <string key="NSWindowTitle">Account</string> - <string key="NSWindowClass">NSWindow</string> - <nil key="NSViewClass"/> - <nil key="NSUserInterfaceItemIdentifier"/> - <object class="NSView" key="NSWindowView" id="370006470"> - <reference key="NSNextResponder"/> - <int key="NSvFlags">256</int> - <array class="NSMutableArray" key="NSSubviews"> - <object class="NSTextField" id="369746882"> - <reference key="NSNextResponder" ref="370006470"/> - <int key="NSvFlags">268</int> - <string key="NSFrame">{{116, 111}, {249, 22}}</string> - <reference key="NSSuperview" ref="370006470"/> - <reference key="NSNextKeyView" ref="47969306"/> - <string key="NSReuseIdentifierKey">_NS:9</string> - <bool key="NSEnabled">YES</bool> - <object class="NSTextFieldCell" key="NSCell" id="163183536"> - <int key="NSCellFlags">-1804599231</int> - <int key="NSCellFlags2">272630784</int> - <string key="NSContents"/> - <reference key="NSSupport" ref="640234263"/> - <string key="NSCellIdentifier">_NS:9</string> - <reference key="NSControlView" ref="369746882"/> - <bool key="NSDrawsBackground">YES</bool> - <object class="NSColor" key="NSBackgroundColor" id="412315037"> - <int key="NSColorSpace">6</int> - <string key="NSCatalogName">System</string> - <string key="NSColorName">textBackgroundColor</string> - <reference key="NSColor" ref="492204717"/> - </object> - <object class="NSColor" key="NSTextColor" id="906820664"> - <int key="NSColorSpace">6</int> - <string key="NSCatalogName">System</string> - <string key="NSColorName">textColor</string> - <reference key="NSColor" ref="342765170"/> - </object> - </object> - <bool key="NSAllowsLogicalLayoutDirection">NO</bool> - </object> - <object class="NSTextField" id="148390580"> - <reference key="NSNextResponder" ref="370006470"/> - <int key="NSvFlags">268</int> - <string key="NSFrame">{{72, 114}, {39, 17}}</string> - <reference key="NSSuperview" ref="370006470"/> - <reference key="NSNextKeyView" ref="369746882"/> - <string key="NSReuseIdentifierKey">_NS:1535</string> - <bool key="NSEnabled">YES</bool> - <object class="NSTextFieldCell" key="NSCell" id="746004588"> - <int key="NSCellFlags">68157504</int> - <int key="NSCellFlags2">272630784</int> - <string key="NSContents">Login</string> - <reference key="NSSupport" ref="640234263"/> - <string key="NSCellIdentifier">_NS:1535</string> - <reference key="NSControlView" ref="148390580"/> - <object class="NSColor" key="NSBackgroundColor" id="809738508"> - <int key="NSColorSpace">6</int> - <string key="NSCatalogName">System</string> - <string key="NSColorName">controlColor</string> - <reference key="NSColor" ref="428798902"/> - </object> - <reference key="NSTextColor" ref="567109269"/> - </object> - <bool key="NSAllowsLogicalLayoutDirection">NO</bool> - </object> - <object class="NSTextField" id="47969306"> - <reference key="NSNextResponder" ref="370006470"/> - <int key="NSvFlags">268</int> - <string key="NSFrame">{{47, 82}, {64, 17}}</string> - <reference key="NSSuperview" ref="370006470"/> - <reference key="NSNextKeyView" ref="217026742"/> - <string key="NSReuseIdentifierKey">_NS:1535</string> - <bool key="NSEnabled">YES</bool> - <object class="NSTextFieldCell" key="NSCell" id="960551038"> - <int key="NSCellFlags">68157504</int> - <int key="NSCellFlags2">272630784</int> - <string key="NSContents">Password</string> - <reference key="NSSupport" ref="640234263"/> - <string key="NSCellIdentifier">_NS:1535</string> - <reference key="NSControlView" ref="47969306"/> - <reference key="NSBackgroundColor" ref="809738508"/> - <reference key="NSTextColor" ref="567109269"/> - </object> - <bool key="NSAllowsLogicalLayoutDirection">NO</bool> - </object> - <object class="NSSecureTextField" id="217026742"> - <reference key="NSNextResponder" ref="370006470"/> - <int key="NSvFlags">268</int> - <string key="NSFrame">{{116, 79}, {249, 22}}</string> - <reference key="NSSuperview" ref="370006470"/> - <reference key="NSNextKeyView" ref="204654299"/> - <string key="NSReuseIdentifierKey">_NS:9</string> - <bool key="NSEnabled">YES</bool> - <object class="NSSecureTextFieldCell" key="NSCell" id="338490228"> - <int key="NSCellFlags">342884416</int> - <int key="NSCellFlags2">272630848</int> - <string key="NSContents"/> - <reference key="NSSupport" ref="640234263"/> - <string key="NSCellIdentifier">_NS:9</string> - <reference key="NSControlView" ref="217026742"/> - <bool key="NSDrawsBackground">YES</bool> - <reference key="NSBackgroundColor" ref="412315037"/> - <reference key="NSTextColor" ref="906820664"/> - <array key="NSAllowedInputLocales"> - <string>NSAllRomanInputSourcesLocaleIdentifier</string> - </array> - </object> - <bool key="NSAllowsLogicalLayoutDirection">NO</bool> - </object> - <object class="NSButton" id="372944324"> - <reference key="NSNextResponder" ref="370006470"/> - <int key="NSvFlags">268</int> - <string key="NSFrame">{{296, 11}, {75, 32}}</string> - <reference key="NSSuperview" ref="370006470"/> - <reference key="NSNextKeyView" ref="758205814"/> - <string key="NSReuseIdentifierKey">_NS:9</string> - <bool key="NSEnabled">YES</bool> - <object class="NSButtonCell" key="NSCell" id="496976666"> - <int key="NSCellFlags">67108864</int> - <int key="NSCellFlags2">134217728</int> - <string key="NSContents">Login</string> - <reference key="NSSupport" ref="640234263"/> - <string key="NSCellIdentifier">_NS:9</string> - <reference key="NSControlView" ref="372944324"/> - <int key="NSButtonFlags">-2038284288</int> - <int key="NSButtonFlags2">129</int> - <string key="NSAlternateContents"/> - <string type="base64-UTF8" key="NSKeyEquivalent">DQ</string> - <int key="NSPeriodicDelay">200</int> - <int key="NSPeriodicInterval">25</int> - </object> - <bool key="NSAllowsLogicalLayoutDirection">NO</bool> - </object> - <object class="NSButton" id="602401876"> - <reference key="NSNextResponder" ref="370006470"/> - <int key="NSvFlags">268</int> - <string key="NSFrame">{{214, 11}, {82, 32}}</string> - <reference key="NSSuperview" ref="370006470"/> - <reference key="NSNextKeyView" ref="372944324"/> - <string key="NSReuseIdentifierKey">_NS:9</string> - <bool key="NSEnabled">YES</bool> - <object class="NSButtonCell" key="NSCell" id="867739186"> - <int key="NSCellFlags">67108864</int> - <int key="NSCellFlags2">134217728</int> - <string key="NSContents">Cancel</string> - <reference key="NSSupport" ref="640234263"/> - <string key="NSCellIdentifier">_NS:9</string> - <reference key="NSControlView" ref="602401876"/> - <int key="NSButtonFlags">-2038284288</int> - <int key="NSButtonFlags2">129</int> - <string key="NSAlternateContents"/> - <string type="base64-UTF8" key="NSKeyEquivalent">Gw</string> - <int key="NSPeriodicDelay">200</int> - <int key="NSPeriodicInterval">25</int> - </object> - <bool key="NSAllowsLogicalLayoutDirection">NO</bool> - </object> - <object class="NSProgressIndicator" id="758205814"> - <reference key="NSNextResponder" ref="370006470"/> - <int key="NSvFlags">268</int> - <string key="NSFrame">{{373, 20}, {16, 16}}</string> - <reference key="NSSuperview" ref="370006470"/> - <reference key="NSNextKeyView"/> - <string key="NSReuseIdentifierKey">_NS:945</string> - <int key="NSpiFlags">28938</int> - <double key="NSMaxValue">100</double> - </object> - <object class="NSTextField" id="1021403255"> - <reference key="NSNextResponder" ref="370006470"/> - <int key="NSvFlags">268</int> - <string key="NSFrame">{{116, 47}, {249, 22}}</string> - <reference key="NSSuperview" ref="370006470"/> - <reference key="NSNextKeyView" ref="602401876"/> - <string key="NSReuseIdentifierKey">_NS:9</string> - <bool key="NSEnabled">YES</bool> - <object class="NSTextFieldCell" key="NSCell" id="828733144"> - <int key="NSCellFlags">-1804599231</int> - <int key="NSCellFlags2">272630784</int> - <string key="NSContents"/> - <reference key="NSSupport" ref="640234263"/> - <string key="NSCellIdentifier">_NS:9</string> - <reference key="NSControlView" ref="1021403255"/> - <bool key="NSDrawsBackground">YES</bool> - <reference key="NSBackgroundColor" ref="412315037"/> - <reference key="NSTextColor" ref="906820664"/> - </object> - <bool key="NSAllowsLogicalLayoutDirection">NO</bool> - </object> - <object class="NSTextField" id="204654299"> - <reference key="NSNextResponder" ref="370006470"/> - <int key="NSvFlags">268</int> - <string key="NSFrame">{{43, 50}, {68, 17}}</string> - <reference key="NSSuperview" ref="370006470"/> - <reference key="NSNextKeyView" ref="1021403255"/> - <string key="NSReuseIdentifierKey">_NS:1535</string> - <bool key="NSEnabled">YES</bool> - <object class="NSTextFieldCell" key="NSCell" id="670594070"> - <int key="NSCellFlags">68157504</int> - <int key="NSCellFlags2">272630784</int> - <string key="NSContents">Hostname</string> - <reference key="NSSupport" ref="640234263"/> - <string key="NSCellIdentifier">_NS:1535</string> - <reference key="NSControlView" ref="204654299"/> - <reference key="NSBackgroundColor" ref="809738508"/> - <reference key="NSTextColor" ref="567109269"/> - </object> - <bool key="NSAllowsLogicalLayoutDirection">NO</bool> - </object> - </array> - <string key="NSFrameSize">{409, 153}</string> - <reference key="NSSuperview"/> - <reference key="NSNextKeyView" ref="148390580"/> - <string key="NSReuseIdentifierKey">_NS:20</string> - </object> - <string key="NSScreenRect">{{0, 0}, {1440, 878}}</string> - <string key="NSMaxSize">{10000000000000, 10000000000000}</string> - <bool key="NSWindowIsRestorable">YES</bool> - </object> - <object class="NSCustomObject" id="976324537"> - <string key="NSClassName">AppDelegate</string> - </object> - <object class="NSCustomObject" id="755631768"> - <string key="NSClassName">NSFontManager</string> - </object> - <object class="NSViewController" id="409855171"/> - <object class="NSViewController" id="477402609"/> - <object class="NSUserDefaultsController" id="522483983"> - <bool key="NSSharedInstance">YES</bool> - </object> - </array> - <object class="IBObjectContainer" key="IBDocument.Objects"> - <array class="NSMutableArray" key="connectionRecords"> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">terminate:</string> - <reference key="source" ref="1050"/> - <reference key="destination" ref="632727374"/> - </object> - <int key="connectionID">449</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">orderFrontStandardAboutPanel:</string> - <reference key="source" ref="1021"/> - <reference key="destination" ref="238522557"/> - </object> - <int key="connectionID">142</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">delegate</string> - <reference key="source" ref="1021"/> - <reference key="destination" ref="976324537"/> - </object> - <int key="connectionID">495</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">performMiniaturize:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="1011231497"/> - </object> - <int key="connectionID">37</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">arrangeInFront:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="625202149"/> - </object> - <int key="connectionID">39</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">performClose:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="776162233"/> - </object> - <int key="connectionID">193</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">toggleContinuousSpellChecking:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="948374510"/> - </object> - <int key="connectionID">222</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">undo:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="1058277027"/> - </object> - <int key="connectionID">223</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">copy:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="860595796"/> - </object> - <int key="connectionID">224</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">checkSpelling:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="96193923"/> - </object> - <int key="connectionID">225</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">paste:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="29853731"/> - </object> - <int key="connectionID">226</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">stopSpeaking:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="680220178"/> - </object> - <int key="connectionID">227</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">cut:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="296257095"/> - </object> - <int key="connectionID">228</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">showGuessPanel:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="679648819"/> - </object> - <int key="connectionID">230</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">redo:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="790794224"/> - </object> - <int key="connectionID">231</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">selectAll:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="583158037"/> - </object> - <int key="connectionID">232</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">startSpeaking:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="731782645"/> - </object> - <int key="connectionID">233</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">delete:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="437104165"/> - </object> - <int key="connectionID">235</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">performZoom:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="575023229"/> - </object> - <int key="connectionID">240</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">performFindPanelAction:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="447796847"/> - </object> - <int key="connectionID">241</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">centerSelectionInVisibleArea:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="88285865"/> - </object> - <int key="connectionID">245</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">toggleGrammarChecking:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="967646866"/> - </object> - <int key="connectionID">347</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">toggleSmartInsertDelete:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="605118523"/> - </object> - <int key="connectionID">355</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">toggleAutomaticQuoteSubstitution:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="197661976"/> - </object> - <int key="connectionID">356</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">toggleAutomaticLinkDetection:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="708854459"/> - </object> - <int key="connectionID">357</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">hide:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="755159360"/> - </object> - <int key="connectionID">367</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">hideOtherApplications:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="342932134"/> - </object> - <int key="connectionID">368</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">unhideAllApplications:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="908899353"/> - </object> - <int key="connectionID">370</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">raiseBaseline:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="941806246"/> - </object> - <int key="connectionID">426</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">lowerBaseline:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="1045724900"/> - </object> - <int key="connectionID">427</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">copyFont:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="596732606"/> - </object> - <int key="connectionID">428</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">subscript:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="1037576581"/> - </object> - <int key="connectionID">429</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">superscript:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="644725453"/> - </object> - <int key="connectionID">430</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">tightenKerning:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="677519740"/> - </object> - <int key="connectionID">431</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">underline:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="330926929"/> - </object> - <int key="connectionID">432</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">orderFrontColorPanel:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="1012600125"/> - </object> - <int key="connectionID">433</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">useAllLigatures:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="663508465"/> - </object> - <int key="connectionID">434</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">loosenKerning:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="238351151"/> - </object> - <int key="connectionID">435</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">pasteFont:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="393423671"/> - </object> - <int key="connectionID">436</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">unscript:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="257962622"/> - </object> - <int key="connectionID">437</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">useStandardKerning:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="252969304"/> - </object> - <int key="connectionID">438</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">useStandardLigatures:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="706297211"/> - </object> - <int key="connectionID">439</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">turnOffLigatures:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="568384683"/> - </object> - <int key="connectionID">440</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">turnOffKerning:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="766922938"/> - </object> - <int key="connectionID">441</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">toggleAutomaticSpellingCorrection:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="795346622"/> - </object> - <int key="connectionID">456</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">orderFrontSubstitutionsPanel:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="65139061"/> - </object> - <int key="connectionID">458</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">toggleAutomaticDashSubstitution:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="672708820"/> - </object> - <int key="connectionID">461</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">toggleAutomaticTextReplacement:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="537092702"/> - </object> - <int key="connectionID">463</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">uppercaseWord:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="1060694897"/> - </object> - <int key="connectionID">464</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">capitalizeWord:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="56570060"/> - </object> - <int key="connectionID">467</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">lowercaseWord:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="879586729"/> - </object> - <int key="connectionID">468</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">pasteAsPlainText:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="82994268"/> - </object> - <int key="connectionID">486</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">performFindPanelAction:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="326711663"/> - </object> - <int key="connectionID">487</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">performFindPanelAction:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="270902937"/> - </object> - <int key="connectionID">488</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">performFindPanelAction:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="159080638"/> - </object> - <int key="connectionID">489</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">showHelp:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="105068016"/> - </object> - <int key="connectionID">493</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">alignCenter:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="630155264"/> - </object> - <int key="connectionID">518</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">pasteRuler:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="883618387"/> - </object> - <int key="connectionID">519</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">toggleRuler:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="644046920"/> - </object> - <int key="connectionID">520</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">alignRight:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="512868991"/> - </object> - <int key="connectionID">521</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">copyRuler:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="231811626"/> - </object> - <int key="connectionID">522</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">alignJustified:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="945678886"/> - </object> - <int key="connectionID">523</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">alignLeft:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="875092757"/> - </object> - <int key="connectionID">524</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">makeBaseWritingDirectionNatural:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="551969625"/> - </object> - <int key="connectionID">525</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">makeBaseWritingDirectionLeftToRight:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="249532473"/> - </object> - <int key="connectionID">526</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">makeBaseWritingDirectionRightToLeft:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="607364498"/> - </object> - <int key="connectionID">527</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">makeTextWritingDirectionNatural:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="380031999"/> - </object> - <int key="connectionID">528</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">makeTextWritingDirectionLeftToRight:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="825984362"/> - </object> - <int key="connectionID">529</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">makeTextWritingDirectionRightToLeft:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="560145579"/> - </object> - <int key="connectionID">530</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">performFindPanelAction:</string> - <reference key="source" ref="1014"/> - <reference key="destination" ref="738670835"/> - </object> - <int key="connectionID">535</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">addFontTrait:</string> - <reference key="source" ref="755631768"/> - <reference key="destination" ref="305399458"/> - </object> - <int key="connectionID">421</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">addFontTrait:</string> - <reference key="source" ref="755631768"/> - <reference key="destination" ref="814362025"/> - </object> - <int key="connectionID">422</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">modifyFont:</string> - <reference key="source" ref="755631768"/> - <reference key="destination" ref="885547335"/> - </object> - <int key="connectionID">423</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">orderFrontFontPanel:</string> - <reference key="source" ref="755631768"/> - <reference key="destination" ref="159677712"/> - </object> - <int key="connectionID">424</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">modifyFont:</string> - <reference key="source" ref="755631768"/> - <reference key="destination" ref="158063935"/> - </object> - <int key="connectionID">425</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">window</string> - <reference key="source" ref="976324537"/> - <reference key="destination" ref="972006081"/> - </object> - <int key="connectionID">532</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">cancelButton</string> - <reference key="source" ref="976324537"/> - <reference key="destination" ref="602401876"/> - </object> - <int key="connectionID">636</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">loginButton</string> - <reference key="source" ref="976324537"/> - <reference key="destination" ref="372944324"/> - </object> - <int key="connectionID">637</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">progressView</string> - <reference key="source" ref="976324537"/> - <reference key="destination" ref="758205814"/> - </object> - <int key="connectionID">638</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">_loginButton</string> - <reference key="source" ref="976324537"/> - <reference key="destination" ref="372944324"/> - </object> - <int key="connectionID">641</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">_cancelButton</string> - <reference key="source" ref="976324537"/> - <reference key="destination" ref="602401876"/> - </object> - <int key="connectionID">642</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">_progressView</string> - <reference key="source" ref="976324537"/> - <reference key="destination" ref="758205814"/> - </object> - <int key="connectionID">643</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">_msgListViewController</string> - <reference key="source" ref="976324537"/> - <reference key="destination" ref="409855171"/> - </object> - <int key="connectionID">644</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">_accountWindow</string> - <reference key="source" ref="976324537"/> - <reference key="destination" ref="389393842"/> - </object> - <int key="connectionID">645</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">accountCancel:</string> - <reference key="source" ref="976324537"/> - <reference key="destination" ref="602401876"/> - </object> - <int key="connectionID">652</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">accountLogin:</string> - <reference key="source" ref="976324537"/> - <reference key="destination" ref="372944324"/> - </object> - <int key="connectionID">653</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">dataSource</string> - <reference key="source" ref="818403152"/> - <reference key="destination" ref="409855171"/> - </object> - <int key="connectionID">597</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">delegate</string> - <reference key="source" ref="818403152"/> - <reference key="destination" ref="409855171"/> - </object> - <int key="connectionID">598</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">hidden: self.loading</string> - <reference key="source" ref="220665056"/> - <reference key="destination" ref="409855171"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="220665056"/> - <reference key="NSDestination" ref="409855171"/> - <string key="NSLabel">hidden: self.loading</string> - <string key="NSBinding">hidden</string> - <string key="NSKeyPath">self.loading</string> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">694</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">_tableView</string> - <reference key="source" ref="409855171"/> - <reference key="destination" ref="818403152"/> - </object> - <int key="connectionID">596</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">_msgViewController</string> - <reference key="source" ref="409855171"/> - <reference key="destination" ref="477402609"/> - </object> - <int key="connectionID">608</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">_messageView</string> - <reference key="source" ref="477402609"/> - <reference key="destination" ref="220665056"/> - </object> - <int key="connectionID">607</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBActionConnection" key="connection"> - <string key="label">makeKeyAndOrderFront:</string> - <reference key="source" ref="389393842"/> - <reference key="destination" ref="354256217"/> - </object> - <int key="connectionID">622</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBOutletConnection" key="connection"> - <string key="label">initialFirstResponder</string> - <reference key="source" ref="389393842"/> - <reference key="destination" ref="369746882"/> - </object> - <int key="connectionID">654</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">value: self.login</string> - <reference key="source" ref="369746882"/> - <reference key="destination" ref="976324537"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="369746882"/> - <reference key="NSDestination" ref="976324537"/> - <string key="NSLabel">value: self.login</string> - <string key="NSBinding">value</string> - <string key="NSKeyPath">self.login</string> - <object class="NSDictionary" key="NSOptions"> - <string key="NS.key.0">NSContinuouslyUpdatesValue</string> - <boolean value="YES" key="NS.object.0"/> - </object> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">680</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">value: self.password</string> - <reference key="source" ref="217026742"/> - <reference key="destination" ref="976324537"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="217026742"/> - <reference key="NSDestination" ref="976324537"/> - <string key="NSLabel">value: self.password</string> - <string key="NSBinding">value</string> - <string key="NSKeyPath">self.password</string> - <object class="NSDictionary" key="NSOptions"> - <string key="NS.key.0">NSContinuouslyUpdatesValue</string> - <boolean value="YES" key="NS.object.0"/> - </object> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">679</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">enabled: self.loginEnabled</string> - <reference key="source" ref="372944324"/> - <reference key="destination" ref="976324537"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="372944324"/> - <reference key="NSDestination" ref="976324537"/> - <string key="NSLabel">enabled: self.loginEnabled</string> - <string key="NSBinding">enabled</string> - <string key="NSKeyPath">self.loginEnabled</string> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">678</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">enabled: self.loggingIn</string> - <reference key="source" ref="602401876"/> - <reference key="destination" ref="976324537"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="602401876"/> - <reference key="NSDestination" ref="976324537"/> - <string key="NSLabel">enabled: self.loggingIn</string> - <string key="NSBinding">enabled</string> - <string key="NSKeyPath">self.loggingIn</string> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">670</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">hidden: self.loggingIn</string> - <reference key="source" ref="758205814"/> - <reference key="destination" ref="976324537"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="758205814"/> - <reference key="NSDestination" ref="976324537"/> - <string key="NSLabel">hidden: self.loggingIn</string> - <string key="NSBinding">hidden</string> - <string key="NSKeyPath">self.loggingIn</string> - <object class="NSDictionary" key="NSOptions"> - <string key="NS.key.0">NSValueTransformerName</string> - <string key="NS.object.0">NSNegateBoolean</string> - </object> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">672</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">animate: self.loggingIn</string> - <reference key="source" ref="758205814"/> - <reference key="destination" ref="976324537"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="758205814"/> - <reference key="NSDestination" ref="976324537"/> - <string key="NSLabel">animate: self.loggingIn</string> - <string key="NSBinding">animate</string> - <string key="NSKeyPath">self.loggingIn</string> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">674</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">value: values.FetchFullMessageEnabled</string> - <reference key="source" ref="521030078"/> - <reference key="destination" ref="522483983"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="521030078"/> - <reference key="NSDestination" ref="522483983"/> - <string key="NSLabel">value: values.FetchFullMessageEnabled</string> - <string key="NSBinding">value</string> - <string key="NSKeyPath">values.FetchFullMessageEnabled</string> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">648</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">value: self.hostname</string> - <reference key="source" ref="1021403255"/> - <reference key="destination" ref="976324537"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="1021403255"/> - <reference key="NSDestination" ref="976324537"/> - <string key="NSLabel">value: self.hostname</string> - <string key="NSBinding">value</string> - <string key="NSKeyPath">self.hostname</string> - <object class="NSDictionary" key="NSOptions"> - <string key="NS.key.0">NSContinuouslyUpdatesValue</string> - <boolean value="YES" key="NS.object.0"/> - </object> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">686</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">hidden: self.loading</string> - <reference key="source" ref="677395401"/> - <reference key="destination" ref="409855171"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="677395401"/> - <reference key="NSDestination" ref="409855171"/> - <string key="NSLabel">hidden: self.loading</string> - <string key="NSBinding">hidden</string> - <string key="NSKeyPath">self.loading</string> - <object class="NSDictionary" key="NSOptions"> - <string key="NS.key.0">NSValueTransformerName</string> - <string key="NS.object.0">NSNegateBoolean</string> - </object> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">689</int> - </object> - <object class="IBConnectionRecord"> - <object class="IBBindingConnection" key="connection"> - <string key="label">animate: self.loading</string> - <reference key="source" ref="677395401"/> - <reference key="destination" ref="409855171"/> - <object class="NSNibBindingConnector" key="connector"> - <reference key="NSSource" ref="677395401"/> - <reference key="NSDestination" ref="409855171"/> - <string key="NSLabel">animate: self.loading</string> - <string key="NSBinding">animate</string> - <string key="NSKeyPath">self.loading</string> - <int key="NSNibBindingConnectorVersion">2</int> - </object> - </object> - <int key="connectionID">691</int> - </object> - </array> - <object class="IBMutableOrderedSet" key="objectRecords"> - <array key="orderedObjects"> - <object class="IBObjectRecord"> - <int key="objectID">0</int> - <array key="object" id="0"/> - <reference key="children" ref="1048"/> - <nil key="parent"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">-2</int> - <reference key="object" ref="1021"/> - <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="1014"/> - <reference key="parent" ref="0"/> - <string key="objectName">First Responder</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">-3</int> - <reference key="object" ref="1050"/> - <reference key="parent" ref="0"/> - <string key="objectName">Application</string> - </object> - <object class="IBObjectRecord"> - <int key="objectID">29</int> - <reference key="object" ref="649796088"/> - <array class="NSMutableArray" key="children"> - <reference ref="713487014"/> - <reference ref="694149608"/> - <reference ref="952259628"/> - <reference ref="379814623"/> - <reference ref="302598603"/> - <reference ref="448692316"/> - </array> - <reference key="parent" ref="0"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">19</int> - <reference key="object" ref="713487014"/> - <array class="NSMutableArray" key="children"> - <reference ref="835318025"/> - </array> - <reference key="parent" ref="649796088"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">56</int> - <reference key="object" ref="694149608"/> - <array class="NSMutableArray" key="children"> - <reference ref="110575045"/> - </array> - <reference key="parent" ref="649796088"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">217</int> - <reference key="object" ref="952259628"/> - <array class="NSMutableArray" key="children"> - <reference ref="789758025"/> - </array> - <reference key="parent" ref="649796088"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">83</int> - <reference key="object" ref="379814623"/> - <array class="NSMutableArray" key="children"> - <reference ref="720053764"/> - </array> - <reference key="parent" ref="649796088"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">81</int> - <reference key="object" ref="720053764"/> - <array class="NSMutableArray" key="children"> - <reference ref="776162233"/> - <reference ref="425164168"/> - <reference ref="354256217"/> - <reference ref="521030078"/> - </array> - <reference key="parent" ref="379814623"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">73</int> - <reference key="object" ref="776162233"/> - <reference key="parent" ref="720053764"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">79</int> - <reference key="object" ref="425164168"/> - <reference key="parent" ref="720053764"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">205</int> - <reference key="object" ref="789758025"/> - <array class="NSMutableArray" key="children"> - <reference ref="437104165"/> - <reference ref="583158037"/> - <reference ref="1058277027"/> - <reference ref="212016141"/> - <reference ref="296257095"/> - <reference ref="29853731"/> - <reference ref="860595796"/> - <reference ref="1040322652"/> - <reference ref="790794224"/> - <reference ref="892235320"/> - <reference ref="972420730"/> - <reference ref="676164635"/> - <reference ref="507821607"/> - <reference ref="288088188"/> - <reference ref="82994268"/> - </array> - <reference key="parent" ref="952259628"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">202</int> - <reference key="object" ref="437104165"/> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">198</int> - <reference key="object" ref="583158037"/> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">207</int> - <reference key="object" ref="1058277027"/> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">214</int> - <reference key="object" ref="212016141"/> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">199</int> - <reference key="object" ref="296257095"/> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">203</int> - <reference key="object" ref="29853731"/> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">197</int> - <reference key="object" ref="860595796"/> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">206</int> - <reference key="object" ref="1040322652"/> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">215</int> - <reference key="object" ref="790794224"/> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">218</int> - <reference key="object" ref="892235320"/> - <array class="NSMutableArray" key="children"> - <reference ref="963351320"/> - </array> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">216</int> - <reference key="object" ref="972420730"/> - <array class="NSMutableArray" key="children"> - <reference ref="769623530"/> - </array> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">200</int> - <reference key="object" ref="769623530"/> - <array class="NSMutableArray" key="children"> - <reference ref="948374510"/> - <reference ref="96193923"/> - <reference ref="679648819"/> - <reference ref="967646866"/> - <reference ref="859480356"/> - <reference ref="795346622"/> - </array> - <reference key="parent" ref="972420730"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">219</int> - <reference key="object" ref="948374510"/> - <reference key="parent" ref="769623530"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">201</int> - <reference key="object" ref="96193923"/> - <reference key="parent" ref="769623530"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">204</int> - <reference key="object" ref="679648819"/> - <reference key="parent" ref="769623530"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">220</int> - <reference key="object" ref="963351320"/> - <array class="NSMutableArray" key="children"> - <reference ref="270902937"/> - <reference ref="88285865"/> - <reference ref="159080638"/> - <reference ref="326711663"/> - <reference ref="447796847"/> - <reference ref="738670835"/> - </array> - <reference key="parent" ref="892235320"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">213</int> - <reference key="object" ref="270902937"/> - <reference key="parent" ref="963351320"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">210</int> - <reference key="object" ref="88285865"/> - <reference key="parent" ref="963351320"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">221</int> - <reference key="object" ref="159080638"/> - <reference key="parent" ref="963351320"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">208</int> - <reference key="object" ref="326711663"/> - <reference key="parent" ref="963351320"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">209</int> - <reference key="object" ref="447796847"/> - <reference key="parent" ref="963351320"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">57</int> - <reference key="object" ref="110575045"/> - <array class="NSMutableArray" key="children"> - <reference ref="238522557"/> - <reference ref="755159360"/> - <reference ref="908899353"/> - <reference ref="632727374"/> - <reference ref="646227648"/> - <reference ref="609285721"/> - <reference ref="481834944"/> - <reference ref="304266470"/> - <reference ref="1046388886"/> - <reference ref="1056857174"/> - <reference ref="342932134"/> - </array> - <reference key="parent" ref="694149608"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">58</int> - <reference key="object" ref="238522557"/> - <reference key="parent" ref="110575045"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">134</int> - <reference key="object" ref="755159360"/> - <reference key="parent" ref="110575045"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">150</int> - <reference key="object" ref="908899353"/> - <reference key="parent" ref="110575045"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">136</int> - <reference key="object" ref="632727374"/> - <reference key="parent" ref="110575045"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">144</int> - <reference key="object" ref="646227648"/> - <reference key="parent" ref="110575045"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">129</int> - <reference key="object" ref="609285721"/> - <reference key="parent" ref="110575045"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">143</int> - <reference key="object" ref="481834944"/> - <reference key="parent" ref="110575045"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">236</int> - <reference key="object" ref="304266470"/> - <reference key="parent" ref="110575045"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">131</int> - <reference key="object" ref="1046388886"/> - <array class="NSMutableArray" key="children"> - <reference ref="752062318"/> - </array> - <reference key="parent" ref="110575045"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">149</int> - <reference key="object" ref="1056857174"/> - <reference key="parent" ref="110575045"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">145</int> - <reference key="object" ref="342932134"/> - <reference key="parent" ref="110575045"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">130</int> - <reference key="object" ref="752062318"/> - <reference key="parent" ref="1046388886"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">24</int> - <reference key="object" ref="835318025"/> - <array class="NSMutableArray" key="children"> - <reference ref="299356726"/> - <reference ref="625202149"/> - <reference ref="575023229"/> - <reference ref="1011231497"/> - </array> - <reference key="parent" ref="713487014"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">92</int> - <reference key="object" ref="299356726"/> - <reference key="parent" ref="835318025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">5</int> - <reference key="object" ref="625202149"/> - <reference key="parent" ref="835318025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">239</int> - <reference key="object" ref="575023229"/> - <reference key="parent" ref="835318025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">23</int> - <reference key="object" ref="1011231497"/> - <reference key="parent" ref="835318025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">211</int> - <reference key="object" ref="676164635"/> - <array class="NSMutableArray" key="children"> - <reference ref="785027613"/> - </array> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">212</int> - <reference key="object" ref="785027613"/> - <array class="NSMutableArray" key="children"> - <reference ref="680220178"/> - <reference ref="731782645"/> - </array> - <reference key="parent" ref="676164635"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">195</int> - <reference key="object" ref="680220178"/> - <reference key="parent" ref="785027613"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">196</int> - <reference key="object" ref="731782645"/> - <reference key="parent" ref="785027613"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">346</int> - <reference key="object" ref="967646866"/> - <reference key="parent" ref="769623530"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">348</int> - <reference key="object" ref="507821607"/> - <array class="NSMutableArray" key="children"> - <reference ref="698887838"/> - </array> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">349</int> - <reference key="object" ref="698887838"/> - <array class="NSMutableArray" key="children"> - <reference ref="605118523"/> - <reference ref="197661976"/> - <reference ref="708854459"/> - <reference ref="65139061"/> - <reference ref="19036812"/> - <reference ref="672708820"/> - <reference ref="537092702"/> - </array> - <reference key="parent" ref="507821607"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">350</int> - <reference key="object" ref="605118523"/> - <reference key="parent" ref="698887838"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">351</int> - <reference key="object" ref="197661976"/> - <reference key="parent" ref="698887838"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">354</int> - <reference key="object" ref="708854459"/> - <reference key="parent" ref="698887838"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">371</int> - <reference key="object" ref="972006081"/> - <array class="NSMutableArray" key="children"> - <reference ref="439893737"/> - </array> - <reference key="parent" ref="0"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">372</int> - <reference key="object" ref="439893737"/> - <array class="NSMutableArray" key="children"> - <reference ref="665197104"/> - <reference ref="220665056"/> - <reference ref="677395401"/> - </array> - <reference key="parent" ref="972006081"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">375</int> - <reference key="object" ref="302598603"/> - <array class="NSMutableArray" key="children"> - <reference ref="941447902"/> - </array> - <reference key="parent" ref="649796088"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">376</int> - <reference key="object" ref="941447902"/> - <array class="NSMutableArray" key="children"> - <reference ref="792887677"/> - <reference ref="215659978"/> - </array> - <reference key="parent" ref="302598603"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">377</int> - <reference key="object" ref="792887677"/> - <array class="NSMutableArray" key="children"> - <reference ref="786677654"/> - </array> - <reference key="parent" ref="941447902"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">388</int> - <reference key="object" ref="786677654"/> - <array class="NSMutableArray" key="children"> - <reference ref="159677712"/> - <reference ref="305399458"/> - <reference ref="814362025"/> - <reference ref="330926929"/> - <reference ref="533507878"/> - <reference ref="158063935"/> - <reference ref="885547335"/> - <reference ref="901062459"/> - <reference ref="767671776"/> - <reference ref="691570813"/> - <reference ref="769124883"/> - <reference ref="739652853"/> - <reference ref="1012600125"/> - <reference ref="214559597"/> - <reference ref="596732606"/> - <reference ref="393423671"/> - </array> - <reference key="parent" ref="792887677"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">389</int> - <reference key="object" ref="159677712"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">390</int> - <reference key="object" ref="305399458"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">391</int> - <reference key="object" ref="814362025"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">392</int> - <reference key="object" ref="330926929"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">393</int> - <reference key="object" ref="533507878"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">394</int> - <reference key="object" ref="158063935"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">395</int> - <reference key="object" ref="885547335"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">396</int> - <reference key="object" ref="901062459"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">397</int> - <reference key="object" ref="767671776"/> - <array class="NSMutableArray" key="children"> - <reference ref="175441468"/> - </array> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">398</int> - <reference key="object" ref="691570813"/> - <array class="NSMutableArray" key="children"> - <reference ref="1058217995"/> - </array> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">399</int> - <reference key="object" ref="769124883"/> - <array class="NSMutableArray" key="children"> - <reference ref="18263474"/> - </array> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">400</int> - <reference key="object" ref="739652853"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">401</int> - <reference key="object" ref="1012600125"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">402</int> - <reference key="object" ref="214559597"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">403</int> - <reference key="object" ref="596732606"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">404</int> - <reference key="object" ref="393423671"/> - <reference key="parent" ref="786677654"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">405</int> - <reference key="object" ref="18263474"/> - <array class="NSMutableArray" key="children"> - <reference ref="257962622"/> - <reference ref="644725453"/> - <reference ref="1037576581"/> - <reference ref="941806246"/> - <reference ref="1045724900"/> - </array> - <reference key="parent" ref="769124883"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">406</int> - <reference key="object" ref="257962622"/> - <reference key="parent" ref="18263474"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">407</int> - <reference key="object" ref="644725453"/> - <reference key="parent" ref="18263474"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">408</int> - <reference key="object" ref="1037576581"/> - <reference key="parent" ref="18263474"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">409</int> - <reference key="object" ref="941806246"/> - <reference key="parent" ref="18263474"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">410</int> - <reference key="object" ref="1045724900"/> - <reference key="parent" ref="18263474"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">411</int> - <reference key="object" ref="1058217995"/> - <array class="NSMutableArray" key="children"> - <reference ref="706297211"/> - <reference ref="568384683"/> - <reference ref="663508465"/> - </array> - <reference key="parent" ref="691570813"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">412</int> - <reference key="object" ref="706297211"/> - <reference key="parent" ref="1058217995"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">413</int> - <reference key="object" ref="568384683"/> - <reference key="parent" ref="1058217995"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">414</int> - <reference key="object" ref="663508465"/> - <reference key="parent" ref="1058217995"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">415</int> - <reference key="object" ref="175441468"/> - <array class="NSMutableArray" key="children"> - <reference ref="252969304"/> - <reference ref="766922938"/> - <reference ref="677519740"/> - <reference ref="238351151"/> - </array> - <reference key="parent" ref="767671776"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">416</int> - <reference key="object" ref="252969304"/> - <reference key="parent" ref="175441468"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">417</int> - <reference key="object" ref="766922938"/> - <reference key="parent" ref="175441468"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">418</int> - <reference key="object" ref="677519740"/> - <reference key="parent" ref="175441468"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">419</int> - <reference key="object" ref="238351151"/> - <reference key="parent" ref="175441468"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">420</int> - <reference key="object" ref="755631768"/> - <reference key="parent" ref="0"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">450</int> - <reference key="object" ref="288088188"/> - <array class="NSMutableArray" key="children"> - <reference ref="579392910"/> - </array> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">451</int> - <reference key="object" ref="579392910"/> - <array class="NSMutableArray" key="children"> - <reference ref="1060694897"/> - <reference ref="879586729"/> - <reference ref="56570060"/> - </array> - <reference key="parent" ref="288088188"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">452</int> - <reference key="object" ref="1060694897"/> - <reference key="parent" ref="579392910"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">453</int> - <reference key="object" ref="859480356"/> - <reference key="parent" ref="769623530"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">454</int> - <reference key="object" ref="795346622"/> - <reference key="parent" ref="769623530"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">457</int> - <reference key="object" ref="65139061"/> - <reference key="parent" ref="698887838"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">459</int> - <reference key="object" ref="19036812"/> - <reference key="parent" ref="698887838"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">460</int> - <reference key="object" ref="672708820"/> - <reference key="parent" ref="698887838"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">462</int> - <reference key="object" ref="537092702"/> - <reference key="parent" ref="698887838"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">465</int> - <reference key="object" ref="879586729"/> - <reference key="parent" ref="579392910"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">466</int> - <reference key="object" ref="56570060"/> - <reference key="parent" ref="579392910"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">485</int> - <reference key="object" ref="82994268"/> - <reference key="parent" ref="789758025"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">490</int> - <reference key="object" ref="448692316"/> - <array class="NSMutableArray" key="children"> - <reference ref="992780483"/> - </array> - <reference key="parent" ref="649796088"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">491</int> - <reference key="object" ref="992780483"/> - <array class="NSMutableArray" key="children"> - <reference ref="105068016"/> - </array> - <reference key="parent" ref="448692316"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">492</int> - <reference key="object" ref="105068016"/> - <reference key="parent" ref="992780483"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">494</int> - <reference key="object" ref="976324537"/> - <reference key="parent" ref="0"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">496</int> - <reference key="object" ref="215659978"/> - <array class="NSMutableArray" key="children"> - <reference ref="446991534"/> - </array> - <reference key="parent" ref="941447902"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">497</int> - <reference key="object" ref="446991534"/> - <array class="NSMutableArray" key="children"> - <reference ref="875092757"/> - <reference ref="630155264"/> - <reference ref="945678886"/> - <reference ref="512868991"/> - <reference ref="163117631"/> - <reference ref="31516759"/> - <reference ref="908105787"/> - <reference ref="644046920"/> - <reference ref="231811626"/> - <reference ref="883618387"/> - </array> - <reference key="parent" ref="215659978"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">498</int> - <reference key="object" ref="875092757"/> - <reference key="parent" ref="446991534"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">499</int> - <reference key="object" ref="630155264"/> - <reference key="parent" ref="446991534"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">500</int> - <reference key="object" ref="945678886"/> - <reference key="parent" ref="446991534"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">501</int> - <reference key="object" ref="512868991"/> - <reference key="parent" ref="446991534"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">502</int> - <reference key="object" ref="163117631"/> - <reference key="parent" ref="446991534"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">503</int> - <reference key="object" ref="31516759"/> - <array class="NSMutableArray" key="children"> - <reference ref="956096989"/> - </array> - <reference key="parent" ref="446991534"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">504</int> - <reference key="object" ref="908105787"/> - <reference key="parent" ref="446991534"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">505</int> - <reference key="object" ref="644046920"/> - <reference key="parent" ref="446991534"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">506</int> - <reference key="object" ref="231811626"/> - <reference key="parent" ref="446991534"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">507</int> - <reference key="object" ref="883618387"/> - <reference key="parent" ref="446991534"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">508</int> - <reference key="object" ref="956096989"/> - <array class="NSMutableArray" key="children"> - <reference ref="257099033"/> - <reference ref="551969625"/> - <reference ref="249532473"/> - <reference ref="607364498"/> - <reference ref="508151438"/> - <reference ref="981751889"/> - <reference ref="380031999"/> - <reference ref="825984362"/> - <reference ref="560145579"/> - </array> - <reference key="parent" ref="31516759"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">509</int> - <reference key="object" ref="257099033"/> - <reference key="parent" ref="956096989"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">510</int> - <reference key="object" ref="551969625"/> - <reference key="parent" ref="956096989"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">511</int> - <reference key="object" ref="249532473"/> - <reference key="parent" ref="956096989"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">512</int> - <reference key="object" ref="607364498"/> - <reference key="parent" ref="956096989"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">513</int> - <reference key="object" ref="508151438"/> - <reference key="parent" ref="956096989"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">514</int> - <reference key="object" ref="981751889"/> - <reference key="parent" ref="956096989"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">515</int> - <reference key="object" ref="380031999"/> - <reference key="parent" ref="956096989"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">516</int> - <reference key="object" ref="825984362"/> - <reference key="parent" ref="956096989"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">517</int> - <reference key="object" ref="560145579"/> - <reference key="parent" ref="956096989"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">534</int> - <reference key="object" ref="738670835"/> - <reference key="parent" ref="963351320"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">536</int> - <reference key="object" ref="665197104"/> - <array class="NSMutableArray" key="children"> - <reference ref="818403152"/> - <reference ref="1027823325"/> - <reference ref="8853202"/> - <reference ref="849854472"/> - </array> - <reference key="parent" ref="439893737"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">537</int> - <reference key="object" ref="818403152"/> - <array class="NSMutableArray" key="children"> - <reference ref="631488934"/> - </array> - <reference key="parent" ref="665197104"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">538</int> - <reference key="object" ref="1027823325"/> - <reference key="parent" ref="665197104"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">539</int> - <reference key="object" ref="8853202"/> - <reference key="parent" ref="665197104"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">540</int> - <reference key="object" ref="849854472"/> - <reference key="parent" ref="665197104"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">541</int> - <reference key="object" ref="631488934"/> - <array class="NSMutableArray" key="children"> - <reference ref="665052716"/> - </array> - <reference key="parent" ref="818403152"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">544</int> - <reference key="object" ref="665052716"/> - <reference key="parent" ref="631488934"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">573</int> - <reference key="object" ref="220665056"/> - <array class="NSMutableArray" key="children"/> - <reference key="parent" ref="439893737"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">594</int> - <reference key="object" ref="409855171"/> - <reference key="parent" ref="0"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">595</int> - <reference key="object" ref="477402609"/> - <reference key="parent" ref="0"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">609</int> - <reference key="object" ref="354256217"/> - <reference key="parent" ref="720053764"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">610</int> - <reference key="object" ref="389393842"/> - <array class="NSMutableArray" key="children"> - <reference ref="370006470"/> - </array> - <reference key="parent" ref="0"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">611</int> - <reference key="object" ref="370006470"/> - <array class="NSMutableArray" key="children"> - <reference ref="369746882"/> - <reference ref="148390580"/> - <reference ref="217026742"/> - <reference ref="47969306"/> - <reference ref="1021403255"/> - <reference ref="204654299"/> - <reference ref="372944324"/> - <reference ref="602401876"/> - <reference ref="758205814"/> - </array> - <reference key="parent" ref="389393842"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">612</int> - <reference key="object" ref="369746882"/> - <array class="NSMutableArray" key="children"> - <reference ref="163183536"/> - </array> - <reference key="parent" ref="370006470"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">613</int> - <reference key="object" ref="163183536"/> - <reference key="parent" ref="369746882"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">616</int> - <reference key="object" ref="148390580"/> - <array class="NSMutableArray" key="children"> - <reference ref="746004588"/> - </array> - <reference key="parent" ref="370006470"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">617</int> - <reference key="object" ref="746004588"/> - <reference key="parent" ref="148390580"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">618</int> - <reference key="object" ref="47969306"/> - <array class="NSMutableArray" key="children"> - <reference ref="960551038"/> - </array> - <reference key="parent" ref="370006470"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">619</int> - <reference key="object" ref="960551038"/> - <reference key="parent" ref="47969306"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">620</int> - <reference key="object" ref="217026742"/> - <array class="NSMutableArray" key="children"> - <reference ref="338490228"/> - </array> - <reference key="parent" ref="370006470"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">621</int> - <reference key="object" ref="338490228"/> - <reference key="parent" ref="217026742"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">623</int> - <reference key="object" ref="522483983"/> - <reference key="parent" ref="0"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">627</int> - <reference key="object" ref="372944324"/> - <array class="NSMutableArray" key="children"> - <reference ref="496976666"/> - </array> - <reference key="parent" ref="370006470"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">628</int> - <reference key="object" ref="496976666"/> - <reference key="parent" ref="372944324"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">629</int> - <reference key="object" ref="602401876"/> - <array class="NSMutableArray" key="children"> - <reference ref="867739186"/> - </array> - <reference key="parent" ref="370006470"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">630</int> - <reference key="object" ref="867739186"/> - <reference key="parent" ref="602401876"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">631</int> - <reference key="object" ref="758205814"/> - <reference key="parent" ref="370006470"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">646</int> - <reference key="object" ref="521030078"/> - <reference key="parent" ref="720053764"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">681</int> - <reference key="object" ref="1021403255"/> - <array class="NSMutableArray" key="children"> - <reference ref="828733144"/> - </array> - <reference key="parent" ref="370006470"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">682</int> - <reference key="object" ref="828733144"/> - <reference key="parent" ref="1021403255"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">684</int> - <reference key="object" ref="204654299"/> - <array class="NSMutableArray" key="children"> - <reference ref="670594070"/> - </array> - <reference key="parent" ref="370006470"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">685</int> - <reference key="object" ref="670594070"/> - <reference key="parent" ref="204654299"/> - </object> - <object class="IBObjectRecord"> - <int key="objectID">687</int> - <reference key="object" ref="677395401"/> - <reference key="parent" ref="439893737"/> - </object> - </array> - </object> - <dictionary class="NSMutableDictionary" key="flattenedProperties"> - <string key="-1.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="-2.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="-3.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="129.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="130.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="131.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="134.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="136.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="143.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="144.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="145.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="149.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="150.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="19.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="195.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="196.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="197.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="198.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="199.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="200.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="201.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="202.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="203.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="204.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="205.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="206.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="207.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="208.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="209.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="210.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="211.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="212.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="213.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="214.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="215.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="216.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="217.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="218.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="219.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="220.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="221.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="23.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="236.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="239.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="24.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="29.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="346.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="348.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="349.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="350.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="351.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="354.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <boolean value="NO" key="371.IBNSWindowAutoPositionCentersHorizontal"/> - <boolean value="NO" key="371.IBNSWindowAutoPositionCentersVertical"/> - <string key="371.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="371.IBWindowTemplateEditedContentRect">{{380, 496}, {480, 360}}</string> - <integer value="1" key="371.NSWindowTemplate.visibleAtLaunch"/> - <string key="372.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="375.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="376.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="377.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="388.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="389.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="390.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="391.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="392.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="393.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="394.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="395.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="396.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="397.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="398.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="399.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="400.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="401.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="402.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="403.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="404.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="405.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="406.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="407.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="408.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="409.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="410.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="411.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="412.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="413.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="414.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="415.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="416.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="417.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="418.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="419.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="420.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="450.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="451.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="452.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="453.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="454.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="457.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="459.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="460.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="462.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="465.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="466.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="485.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="490.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="491.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="492.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="494.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="496.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="497.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="498.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="499.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="5.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="500.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="501.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="502.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="503.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="504.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="505.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="506.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="507.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="508.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="509.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="510.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="511.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="512.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="513.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="514.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="515.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="516.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="517.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="534.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="536.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="537.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="538.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="539.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="540.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="541.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="544.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="56.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="57.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="573.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="58.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="594.CustomClassName">MCTMsgListViewController</string> - <string key="594.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="595.CustomClassName">MCTMsgViewController</string> - <string key="595.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="609.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <boolean value="YES" key="610.IBNSWindowAutoPositionCentersHorizontal"/> - <boolean value="YES" key="610.IBNSWindowAutoPositionCentersVertical"/> - <string key="610.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <boolean value="NO" key="610.NSWindowTemplate.visibleAtLaunch"/> - <string key="611.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="612.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="613.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="616.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="617.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="618.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="619.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="620.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="621.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="623.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="627.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="628.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="629.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="630.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="631.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="646.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="681.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="682.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="684.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="685.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="687.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="73.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="79.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="81.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="83.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - <string key="92.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string> - </dictionary> - <dictionary class="NSMutableDictionary" key="unlocalizedProperties"/> - <nil key="activeLocalization"/> - <dictionary class="NSMutableDictionary" key="localizations"/> - <nil key="sourceID"/> - <int key="maxID">694</int> - </object> - <object class="IBClassDescriber" key="IBDocument.Classes"> - <array class="NSMutableArray" key="referencedPartialClassDescriptions"> - <object class="IBPartialClassDescription"> - <string key="className">AppDelegate</string> - <string key="superclassName">UIResponder</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBProjectSource</string> - <string key="minorKey">./Classes/AppDelegate.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">MCOMessageView</string> - <string key="superclassName">NSView</string> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBProjectSource</string> - <string key="minorKey">./Classes/MCOMessageView.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">MCTMsgListViewController</string> - <string key="superclassName">NSViewController</string> - <dictionary class="NSMutableDictionary" key="outlets"> - <string key="_msgViewController">MCTMsgViewController</string> - <string key="_tableView">NSTableView</string> - </dictionary> - <dictionary class="NSMutableDictionary" key="toOneOutletInfosByName"> - <object class="IBToOneOutletInfo" key="_msgViewController"> - <string key="name">_msgViewController</string> - <string key="candidateClassName">MCTMsgViewController</string> - </object> - <object class="IBToOneOutletInfo" key="_tableView"> - <string key="name">_tableView</string> - <string key="candidateClassName">NSTableView</string> - </object> - </dictionary> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBProjectSource</string> - <string key="minorKey">./Classes/MCTMsgListViewController.h</string> - </object> - </object> - <object class="IBPartialClassDescription"> - <string key="className">MCTMsgViewController</string> - <string key="superclassName">NSViewController</string> - <object class="NSMutableDictionary" key="outlets"> - <string key="NS.key.0">_messageView</string> - <string key="NS.object.0">MCOMessageView</string> - </object> - <object class="NSMutableDictionary" key="toOneOutletInfosByName"> - <string key="NS.key.0">_messageView</string> - <object class="IBToOneOutletInfo" key="NS.object.0"> - <string key="name">_messageView</string> - <string key="candidateClassName">MCOMessageView</string> - </object> - </object> - <object class="IBClassDescriptionSource" key="sourceIdentifier"> - <string key="majorKey">IBProjectSource</string> - <string key="minorKey">./Classes/MCTMsgViewController.h</string> - </object> - </object> - </array> - </object> - <int key="IBDocument.localizationMode">0</int> - <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string> - <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool> - <int key="IBDocument.defaultPropertyAccessControl">3</int> - <dictionary class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes"> - <string key="NSMenuCheckmark">{11, 11}</string> - <string key="NSMenuMixedState">{10, 3}</string> - </dictionary> - </data> -</archive> +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="4457.6" systemVersion="12E55" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none"> + <dependencies> + <deployment defaultVersion="1080" identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="4457.6"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> + <connections> + <action selector="orderFrontStandardAboutPanel:" destination="58" id="142"/> + <outlet property="delegate" destination="494" id="495"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"> + <connections> + <action selector="alignCenter:" destination="499" id="518"/> + <action selector="alignJustified:" destination="500" id="523"/> + <action selector="alignLeft:" destination="498" id="524"/> + <action selector="alignRight:" destination="501" id="521"/> + <action selector="arrangeInFront:" destination="5" id="39"/> + <action selector="capitalizeWord:" destination="466" id="467"/> + <action selector="centerSelectionInVisibleArea:" destination="210" id="245"/> + <action selector="checkSpelling:" destination="201" id="225"/> + <action selector="copy:" destination="197" id="224"/> + <action selector="copyFont:" destination="403" id="428"/> + <action selector="copyRuler:" destination="506" id="522"/> + <action selector="cut:" destination="199" id="228"/> + <action selector="delete:" destination="202" id="235"/> + <action selector="hide:" destination="134" id="367"/> + <action selector="hideOtherApplications:" destination="145" id="368"/> + <action selector="loosenKerning:" destination="419" id="435"/> + <action selector="lowerBaseline:" destination="410" id="427"/> + <action selector="lowercaseWord:" destination="465" id="468"/> + <action selector="makeBaseWritingDirectionLeftToRight:" destination="511" id="526"/> + <action selector="makeBaseWritingDirectionNatural:" destination="510" id="525"/> + <action selector="makeBaseWritingDirectionRightToLeft:" destination="512" id="527"/> + <action selector="makeTextWritingDirectionLeftToRight:" destination="516" id="529"/> + <action selector="makeTextWritingDirectionNatural:" destination="515" id="528"/> + <action selector="makeTextWritingDirectionRightToLeft:" destination="517" id="530"/> + <action selector="orderFrontColorPanel:" destination="401" id="433"/> + <action selector="orderFrontSubstitutionsPanel:" destination="457" id="458"/> + <action selector="paste:" destination="203" id="226"/> + <action selector="pasteAsPlainText:" destination="485" id="486"/> + <action selector="pasteFont:" destination="404" id="436"/> + <action selector="pasteRuler:" destination="507" id="519"/> + <action selector="performClose:" destination="73" id="193"/> + <action selector="performFindPanelAction:" destination="209" id="241"/> + <action selector="performFindPanelAction:" destination="208" id="487"/> + <action selector="performFindPanelAction:" destination="213" id="488"/> + <action selector="performFindPanelAction:" destination="221" id="489"/> + <action selector="performFindPanelAction:" destination="534" id="535"/> + <action selector="performMiniaturize:" destination="23" id="37"/> + <action selector="performZoom:" destination="239" id="240"/> + <action selector="raiseBaseline:" destination="409" id="426"/> + <action selector="redo:" destination="215" id="231"/> + <action selector="selectAll:" destination="198" id="232"/> + <action selector="showGuessPanel:" destination="204" id="230"/> + <action selector="showHelp:" destination="492" id="493"/> + <action selector="startSpeaking:" destination="196" id="233"/> + <action selector="stopSpeaking:" destination="195" id="227"/> + <action selector="subscript:" destination="408" id="429"/> + <action selector="superscript:" destination="407" id="430"/> + <action selector="tightenKerning:" destination="418" id="431"/> + <action selector="toggleAutomaticDashSubstitution:" destination="460" id="461"/> + <action selector="toggleAutomaticLinkDetection:" destination="354" id="357"/> + <action selector="toggleAutomaticQuoteSubstitution:" destination="351" id="356"/> + <action selector="toggleAutomaticSpellingCorrection:" destination="454" id="456"/> + <action selector="toggleAutomaticTextReplacement:" destination="462" id="463"/> + <action selector="toggleContinuousSpellChecking:" destination="219" id="222"/> + <action selector="toggleGrammarChecking:" destination="346" id="347"/> + <action selector="toggleRuler:" destination="505" id="520"/> + <action selector="toggleSmartInsertDelete:" destination="350" id="355"/> + <action selector="turnOffKerning:" destination="417" id="441"/> + <action selector="turnOffLigatures:" destination="413" id="440"/> + <action selector="underline:" destination="392" id="432"/> + <action selector="undo:" destination="207" id="223"/> + <action selector="unhideAllApplications:" destination="150" id="370"/> + <action selector="unscript:" destination="406" id="437"/> + <action selector="uppercaseWord:" destination="452" id="464"/> + <action selector="useAllLigatures:" destination="414" id="434"/> + <action selector="useStandardKerning:" destination="416" id="438"/> + <action selector="useStandardLigatures:" destination="412" id="439"/> + </connections> + </customObject> + <customObject id="-3" userLabel="Application"> + <connections> + <action selector="terminate:" destination="136" id="449"/> + </connections> + </customObject> + <menu title="AMainMenu" systemMenu="main" id="29"> + <items> + <menuItem title="macExample" id="56"> + <menu key="submenu" title="macExample" systemMenu="apple" id="57"> + <items> + <menuItem title="About macExample" id="58"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem isSeparatorItem="YES" id="236"> + <modifierMask key="keyEquivalentModifierMask" command="YES"/> + </menuItem> + <menuItem title="Preferences…" keyEquivalent="," id="129"/> + <menuItem isSeparatorItem="YES" id="143"> + <modifierMask key="keyEquivalentModifierMask" command="YES"/> + </menuItem> + <menuItem title="Services" id="131"> + <menu key="submenu" title="Services" systemMenu="services" id="130"/> + </menuItem> + <menuItem isSeparatorItem="YES" id="144"> + <modifierMask key="keyEquivalentModifierMask" command="YES"/> + </menuItem> + <menuItem title="Hide macExample" keyEquivalent="h" id="134"/> + <menuItem title="Hide Others" keyEquivalent="h" id="145"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + </menuItem> + <menuItem title="Show All" id="150"/> + <menuItem isSeparatorItem="YES" id="149"> + <modifierMask key="keyEquivalentModifierMask" command="YES"/> + </menuItem> + <menuItem title="Quit testUI" keyEquivalent="q" id="136"/> + </items> + </menu> + </menuItem> + <menuItem title="File" id="83"> + <menu key="submenu" title="File" id="81"> + <items> + <menuItem title="Account..." id="609"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Fetch Full Message" id="646"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <binding destination="623" name="value" keyPath="values.FetchFullMessageEnabled" id="648"/> + </connections> + </menuItem> + <menuItem title="Use OAuth 2.0" id="rf2-ay-8rn"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <binding destination="623" name="value" keyPath="values.OAuth2Enabled" id="aLG-X1-MaV"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="79"> + <modifierMask key="keyEquivalentModifierMask" command="YES"/> + </menuItem> + <menuItem title="Close" keyEquivalent="w" id="73"/> + </items> + </menu> + </menuItem> + <menuItem title="Edit" id="217"> + <menu key="submenu" title="Edit" id="205"> + <items> + <menuItem title="Undo" keyEquivalent="z" id="207"/> + <menuItem title="Redo" keyEquivalent="Z" id="215"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem isSeparatorItem="YES" id="206"> + <modifierMask key="keyEquivalentModifierMask" command="YES"/> + </menuItem> + <menuItem title="Cut" keyEquivalent="x" id="199"/> + <menuItem title="Copy" keyEquivalent="c" id="197"/> + <menuItem title="Paste" keyEquivalent="v" id="203"/> + <menuItem title="Paste and Match Style" keyEquivalent="V" id="485"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + </menuItem> + <menuItem title="Delete" id="202"/> + <menuItem title="Select All" keyEquivalent="a" id="198"/> + <menuItem isSeparatorItem="YES" id="214"> + <modifierMask key="keyEquivalentModifierMask" command="YES"/> + </menuItem> + <menuItem title="Find" id="218"> + <menu key="submenu" title="Find" id="220"> + <items> + <menuItem title="Find…" tag="1" keyEquivalent="f" id="209"/> + <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="534"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + </menuItem> + <menuItem title="Find Next" tag="2" keyEquivalent="g" id="208"/> + <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="213"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="221"/> + <menuItem title="Jump to Selection" keyEquivalent="j" id="210"/> + </items> + </menu> + </menuItem> + <menuItem title="Spelling and Grammar" id="216"> + <menu key="submenu" title="Spelling and Grammar" id="200"> + <items> + <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="204"/> + <menuItem title="Check Document Now" keyEquivalent=";" id="201"/> + <menuItem isSeparatorItem="YES" id="453"/> + <menuItem title="Check Spelling While Typing" id="219"/> + <menuItem title="Check Grammar With Spelling" id="346"/> + <menuItem title="Correct Spelling Automatically" id="454"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Substitutions" id="348"> + <menu key="submenu" title="Substitutions" id="349"> + <items> + <menuItem title="Show Substitutions" id="457"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem isSeparatorItem="YES" id="459"/> + <menuItem title="Smart Copy/Paste" tag="1" keyEquivalent="f" id="350"/> + <menuItem title="Smart Quotes" tag="2" keyEquivalent="g" id="351"/> + <menuItem title="Smart Dashes" id="460"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Smart Links" tag="3" keyEquivalent="G" id="354"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Text Replacement" id="462"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Transformations" id="450"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Transformations" id="451"> + <items> + <menuItem title="Make Upper Case" id="452"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Make Lower Case" id="465"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Capitalize" id="466"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Speech" id="211"> + <menu key="submenu" title="Speech" id="212"> + <items> + <menuItem title="Start Speaking" id="196"/> + <menuItem title="Stop Speaking" id="195"/> + </items> + </menu> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Format" id="375"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Format" id="376"> + <items> + <menuItem title="Font" id="377"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Font" systemMenu="font" id="388"> + <items> + <menuItem title="Show Fonts" keyEquivalent="t" id="389"/> + <menuItem title="Bold" tag="2" keyEquivalent="b" id="390"/> + <menuItem title="Italic" tag="1" keyEquivalent="i" id="391"/> + <menuItem title="Underline" keyEquivalent="u" id="392"/> + <menuItem isSeparatorItem="YES" id="393"/> + <menuItem title="Bigger" tag="3" keyEquivalent="+" id="394"/> + <menuItem title="Smaller" tag="4" keyEquivalent="-" id="395"/> + <menuItem isSeparatorItem="YES" id="396"/> + <menuItem title="Kern" id="397"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Kern" id="415"> + <items> + <menuItem title="Use Default" id="416"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Use None" id="417"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Tighten" id="418"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Loosen" id="419"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Ligatures" id="398"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Ligatures" id="411"> + <items> + <menuItem title="Use Default" id="412"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Use None" id="413"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Use All" id="414"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Baseline" id="399"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Baseline" id="405"> + <items> + <menuItem title="Use Default" id="406"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Superscript" id="407"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Subscript" id="408"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Raise" id="409"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Lower" id="410"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem isSeparatorItem="YES" id="400"/> + <menuItem title="Show Colors" keyEquivalent="C" id="401"/> + <menuItem isSeparatorItem="YES" id="402"/> + <menuItem title="Copy Style" keyEquivalent="c" id="403"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + </menuItem> + <menuItem title="Paste Style" keyEquivalent="v" id="404"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Text" id="496"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Text" id="497"> + <items> + <menuItem title="Align Left" keyEquivalent="{" id="498"/> + <menuItem title="Center" keyEquivalent="|" id="499"/> + <menuItem title="Justify" id="500"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Align Right" keyEquivalent="}" id="501"/> + <menuItem isSeparatorItem="YES" id="502"/> + <menuItem title="Writing Direction" id="503"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Writing Direction" id="508"> + <items> + <menuItem title="Paragraph" enabled="NO" id="509"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem id="510"> + <string key="title"> Default</string> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem id="511"> + <string key="title"> Left to Right</string> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem id="512"> + <string key="title"> Right to Left</string> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem isSeparatorItem="YES" id="513"/> + <menuItem title="Selection" enabled="NO" id="514"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem id="515"> + <string key="title"> Default</string> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem id="516"> + <string key="title"> Left to Right</string> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem id="517"> + <string key="title"> Right to Left</string> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem isSeparatorItem="YES" id="504"/> + <menuItem title="Show Ruler" id="505"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Copy Ruler" keyEquivalent="c" id="506"> + <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> + </menuItem> + <menuItem title="Paste Ruler" keyEquivalent="v" id="507"> + <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> + </menuItem> + </items> + </menu> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Window" id="19"> + <menu key="submenu" title="Window" systemMenu="window" id="24"> + <items> + <menuItem title="Minimize" keyEquivalent="m" id="23"/> + <menuItem title="Zoom" id="239"/> + <menuItem isSeparatorItem="YES" id="92"> + <modifierMask key="keyEquivalentModifierMask" command="YES"/> + </menuItem> + <menuItem title="Bring All to Front" id="5"/> + </items> + </menu> + </menuItem> + <menuItem title="Help" id="490"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Help" systemMenu="help" id="491"> + <items> + <menuItem title="macExample Help" keyEquivalent="?" id="492"/> + </items> + </menu> + </menuItem> + </items> + </menu> + <window title="testUI" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="371"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/> + <windowPositionMask key="initialPositionMask" leftStrut="YES" topStrut="YES"/> + <rect key="contentRect" x="344" y="127" width="768" height="493"/> + <rect key="screenRect" x="0.0" y="0.0" width="1440" height="878"/> + <view key="contentView" wantsLayer="YES" id="372"> + <rect key="frame" x="0.0" y="0.0" width="768" height="493"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <scrollView autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" id="536"> + <rect key="frame" x="-1" y="0.0" width="194" height="495"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES"/> + <clipView key="contentView" id="NhN-6Q-DSk"> + <rect key="frame" x="1" y="17" width="192" height="477"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnSelection="YES" multipleSelection="NO" autosaveColumns="NO" headerView="539" id="537"> + <rect key="frame" x="0.0" y="0.0" width="192" height="477"/> + <autoresizingMask key="autoresizingMask"/> + <size key="intercellSpacing" width="3" height="2"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/> + <tableColumns> + <tableColumn width="188.68359375" minWidth="40" maxWidth="1000" id="541"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" white="0.33333298560000002" alpha="1" colorSpace="calibratedWhite"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="544"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + </tableColumn> + </tableColumns> + <connections> + <outlet property="dataSource" destination="594" id="597"/> + <outlet property="delegate" destination="594" id="598"/> + </connections> + </tableView> + </subviews> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </clipView> + <scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" id="538"> + <rect key="frame" x="1" y="478" width="335" height="16"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + <scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" id="540"> + <rect key="frame" x="224" y="17" width="15" height="102"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + <tableHeaderView key="headerView" id="539"> + <rect key="frame" x="0.0" y="0.0" width="192" height="17"/> + <autoresizingMask key="autoresizingMask"/> + </tableHeaderView> + </scrollView> + <customView id="573" customClass="MCOMessageView"> + <rect key="frame" x="201" y="0.0" width="567" height="493"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <connections> + <binding destination="594" name="hidden" keyPath="self.loading" id="694"/> + </connections> + </customView> + <progressIndicator horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" bezeled="NO" indeterminate="YES" style="spinning" id="687"> + <rect key="frame" x="468" y="230" width="32" height="32"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <connections> + <binding destination="594" name="hidden" keyPath="self.loading" id="689"> + <dictionary key="options"> + <string key="NSValueTransformerName">NSNegateBoolean</string> + </dictionary> + </binding> + <binding destination="594" name="animate" keyPath="self.loading" id="691"/> + </connections> + </progressIndicator> + </subviews> + </view> + </window> + <window title="Account" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="610"> + <windowStyleMask key="styleMask" titled="YES" closable="YES"/> + <rect key="contentRect" x="505" y="474" width="409" height="153"/> + <rect key="screenRect" x="0.0" y="0.0" width="1440" height="878"/> + <view key="contentView" id="611"> + <rect key="frame" x="0.0" y="0.0" width="409" height="153"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <textField verticalHuggingPriority="750" id="612"> + <rect key="frame" x="116" y="111" width="249" height="22"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="613"> + <font key="font" metaFont="system"/> + <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <binding destination="494" name="value" keyPath="self.login" id="680"> + <dictionary key="options"> + <bool key="NSContinuouslyUpdatesValue" value="YES"/> + </dictionary> + </binding> + </connections> + </textField> + <textField verticalHuggingPriority="750" id="616"> + <rect key="frame" x="72" y="114" width="39" height="17"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Login" id="617"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <textField verticalHuggingPriority="750" id="618"> + <rect key="frame" x="47" y="82" width="64" height="17"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Password" id="619"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <secureTextField verticalHuggingPriority="750" id="620"> + <rect key="frame" x="116" y="79" width="249" height="22"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="621"> + <font key="font" metaFont="system"/> + <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> + <allowedInputSourceLocales> + <string>NSAllRomanInputSourcesLocaleIdentifier</string> + </allowedInputSourceLocales> + </secureTextFieldCell> + <connections> + <binding destination="494" name="value" keyPath="self.password" id="679"> + <dictionary key="options"> + <bool key="NSContinuouslyUpdatesValue" value="YES"/> + </dictionary> + </binding> + </connections> + </secureTextField> + <button verticalHuggingPriority="750" id="627"> + <rect key="frame" x="296" y="11" width="75" height="32"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <buttonCell key="cell" type="push" title="Login" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="628"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + <string key="keyEquivalent" base64-UTF8="YES"> +DQ +</string> + </buttonCell> + <connections> + <binding destination="494" name="enabled" keyPath="self.loginEnabled" id="678"/> + </connections> + </button> + <button verticalHuggingPriority="750" id="629"> + <rect key="frame" x="214" y="11" width="82" height="32"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="630"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + <string key="keyEquivalent" base64-UTF8="YES"> +Gw +</string> + </buttonCell> + <connections> + <binding destination="494" name="enabled" keyPath="self.loggingIn" id="670"/> + </connections> + </button> + <progressIndicator horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" id="631"> + <rect key="frame" x="373" y="20" width="16" height="16"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <connections> + <binding destination="494" name="hidden" keyPath="self.loggingIn" id="672"> + <dictionary key="options"> + <string key="NSValueTransformerName">NSNegateBoolean</string> + </dictionary> + </binding> + <binding destination="494" name="animate" keyPath="self.loggingIn" id="674"/> + </connections> + </progressIndicator> + <textField verticalHuggingPriority="750" id="681"> + <rect key="frame" x="116" y="47" width="249" height="22"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="682"> + <font key="font" metaFont="system"/> + <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <connections> + <binding destination="494" name="value" keyPath="self.hostname" id="686"> + <dictionary key="options"> + <bool key="NSContinuouslyUpdatesValue" value="YES"/> + </dictionary> + </binding> + </connections> + </textField> + <textField verticalHuggingPriority="750" id="684"> + <rect key="frame" x="43" y="50" width="68" height="17"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Hostname" id="685"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + </subviews> + </view> + <connections> + <action selector="makeKeyAndOrderFront:" destination="609" id="622"/> + <outlet property="initialFirstResponder" destination="612" id="654"/> + </connections> + </window> + <customObject id="494" customClass="AppDelegate"> + <connections> + <action selector="accountCancel:" destination="629" id="652"/> + <action selector="accountLogin:" destination="627" id="653"/> + <outlet property="_accountWindow" destination="610" id="645"/> + <outlet property="_cancelButton" destination="629" id="642"/> + <outlet property="_loginButton" destination="627" id="641"/> + <outlet property="_msgListViewController" destination="594" id="644"/> + <outlet property="_progressView" destination="631" id="643"/> + <outlet property="cancelButton" destination="629" id="636"/> + <outlet property="loginButton" destination="627" id="637"/> + <outlet property="progressView" destination="631" id="638"/> + <outlet property="window" destination="371" id="532"/> + </connections> + </customObject> + <customObject id="420" customClass="NSFontManager"> + <connections> + <action selector="addFontTrait:" destination="390" id="421"/> + <action selector="addFontTrait:" destination="391" id="422"/> + <action selector="modifyFont:" destination="395" id="423"/> + <action selector="modifyFont:" destination="394" id="425"/> + <action selector="orderFrontFontPanel:" destination="389" id="424"/> + </connections> + </customObject> + <viewController id="594" customClass="MCTMsgListViewController"> + <connections> + <outlet property="_msgViewController" destination="595" id="608"/> + <outlet property="_tableView" destination="537" id="596"/> + </connections> + </viewController> + <viewController id="595" customClass="MCTMsgViewController"> + <connections> + <outlet property="_messageView" destination="573" id="607"/> + </connections> + </viewController> + <userDefaultsController id="623"/> + </objects> + <classes> + <class className="AppDelegate" superclassName="NSObject"> + <source key="sourceIdentifier" type="project" relativePath="./Classes/AppDelegate.h"/> + <relationships> + <relationship kind="action" name="accountCancel:"/> + <relationship kind="action" name="accountLogin:"/> + <relationship kind="outlet" name="_accountWindow" candidateClass="NSWindow"/> + <relationship kind="outlet" name="_msgListViewController" candidateClass="MCTMsgListViewController"/> + </relationships> + </class> + <class className="MCOMessageView" superclassName="NSView"> + <source key="sourceIdentifier" type="project" relativePath="./Classes/MCOMessageView.h"/> + </class> + <class className="MCTMsgListViewController" superclassName="NSViewController"> + <source key="sourceIdentifier" type="project" relativePath="./Classes/MCTMsgListViewController.h"/> + <relationships> + <relationship kind="outlet" name="_msgViewController" candidateClass="MCTMsgViewController"/> + <relationship kind="outlet" name="_tableView" candidateClass="NSTableView"/> + </relationships> + </class> + <class className="MCTMsgViewController" superclassName="NSViewController"> + <source key="sourceIdentifier" type="project" relativePath="./Classes/MCTMsgViewController.h"/> + <relationships> + <relationship kind="outlet" name="_messageView" candidateClass="MCOMessageView"/> + </relationships> + </class> + </classes> +</document>
\ No newline at end of file |