Archive Project
This commit is contained in:
187
OAuth 2/GTMHTTPFetchHistory.h
Executable file
187
OAuth 2/GTMHTTPFetchHistory.h
Executable file
@@ -0,0 +1,187 @@
|
||||
/* Copyright (c) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//
|
||||
// GTMHTTPFetchHistory.h
|
||||
//
|
||||
|
||||
//
|
||||
// Users of the GTMHTTPFetcher class may optionally create and set a fetch
|
||||
// history object. The fetch history provides "memory" between subsequent
|
||||
// fetches, including:
|
||||
//
|
||||
// - For fetch responses with Etag headers, the fetch history
|
||||
// remembers the response headers. Future fetcher requests to the same URL
|
||||
// will be given an "If-None-Match" header, telling the server to return
|
||||
// a 304 Not Modified status if the response is unchanged, reducing the
|
||||
// server load and network traffic.
|
||||
//
|
||||
// - Optionally, the fetch history can cache the ETagged data that was returned
|
||||
// in the responses that contained Etag headers. If a later fetch
|
||||
// results in a 304 status, the fetcher will return the cached ETagged data
|
||||
// to the client along with a 200 status, hiding the 304.
|
||||
//
|
||||
// - The fetch history can track cookies.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "GTMHTTPFetcher.h"
|
||||
|
||||
#undef _EXTERN
|
||||
#undef _INITIALIZE_AS
|
||||
#ifdef GTMHTTPFETCHHISTORY_DEFINE_GLOBALS
|
||||
#define _EXTERN
|
||||
#define _INITIALIZE_AS(x) =x
|
||||
#else
|
||||
#if defined(__cplusplus)
|
||||
#define _EXTERN extern "C"
|
||||
#else
|
||||
#define _EXTERN extern
|
||||
#endif
|
||||
#define _INITIALIZE_AS(x)
|
||||
#endif
|
||||
|
||||
|
||||
// default data cache size for when we're caching responses to handle "not
|
||||
// modified" errors for the client
|
||||
#if GTM_IPHONE
|
||||
// iPhone: up to 1MB memory
|
||||
_EXTERN const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity _INITIALIZE_AS(1*1024*1024);
|
||||
#else
|
||||
// Mac OS X: up to 15MB memory
|
||||
_EXTERN const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity _INITIALIZE_AS(15*1024*1024);
|
||||
#endif
|
||||
|
||||
// forward declarations
|
||||
@class GTMURLCache;
|
||||
@class GTMCookieStorage;
|
||||
|
||||
@interface GTMHTTPFetchHistory : NSObject <GTMHTTPFetchHistoryProtocol> {
|
||||
@private
|
||||
GTMURLCache *etaggedDataCache_;
|
||||
BOOL shouldRememberETags_;
|
||||
BOOL shouldCacheETaggedData_; // if NO, then only headers are cached
|
||||
GTMCookieStorage *cookieStorage_;
|
||||
}
|
||||
|
||||
// With caching enabled, previously-cached data will be returned instead of
|
||||
// 304 Not Modified responses when repeating a fetch of an URL that previously
|
||||
// included an ETag header in its response
|
||||
@property (assign) BOOL shouldRememberETags; // default: NO
|
||||
@property (assign) BOOL shouldCacheETaggedData; // default: NO
|
||||
|
||||
// the default ETag data cache capacity is kGTMDefaultETaggedDataCacheMemoryCapacity
|
||||
@property (assign) NSUInteger memoryCapacity;
|
||||
|
||||
@property (retain) GTMCookieStorage *cookieStorage;
|
||||
|
||||
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes
|
||||
shouldCacheETaggedData:(BOOL)shouldCacheETaggedData;
|
||||
|
||||
- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet;
|
||||
|
||||
- (void)clearETaggedDataCache;
|
||||
- (void)clearHistory;
|
||||
|
||||
- (void)removeAllCookies;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// GTMURLCache and GTMCachedURLResponse have interfaces similar to their
|
||||
// NSURLCache counterparts, in hopes that someday the NSURLCache versions
|
||||
// can be used. But in 10.5.8, those are not reliable enough except when
|
||||
// used with +setSharedURLCache. Our goal here is just to cache
|
||||
// responses for handling If-None-Match requests that return
|
||||
// "Not Modified" responses, not for replacing the general URL
|
||||
// caches.
|
||||
|
||||
@interface GTMCachedURLResponse : NSObject {
|
||||
@private
|
||||
NSURLResponse *response_;
|
||||
NSData *data_;
|
||||
NSDate *useDate_; // date this response was last saved or used
|
||||
NSDate *reservationDate_; // date this response's ETag was used
|
||||
}
|
||||
|
||||
@property (readonly) NSURLResponse* response;
|
||||
@property (readonly) NSData* data;
|
||||
|
||||
// date the response was saved or last accessed
|
||||
@property (retain) NSDate *useDate;
|
||||
|
||||
// date the response's ETag header was last used for a fetch request
|
||||
@property (retain) NSDate *reservationDate;
|
||||
|
||||
- (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data;
|
||||
@end
|
||||
|
||||
@interface GTMURLCache : NSObject {
|
||||
NSMutableDictionary *responses_; // maps request URL to GTMCachedURLResponse
|
||||
NSUInteger memoryCapacity_; // capacity of NSDatas in the responses
|
||||
NSUInteger totalDataSize_; // sum of sizes of NSDatas of all responses
|
||||
NSTimeInterval reservationInterval_; // reservation expiration interval
|
||||
}
|
||||
|
||||
@property (assign) NSUInteger memoryCapacity;
|
||||
|
||||
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes;
|
||||
|
||||
- (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request;
|
||||
- (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
|
||||
- (void)removeCachedResponseForRequest:(NSURLRequest *)request;
|
||||
- (void)removeAllCachedResponses;
|
||||
|
||||
// for unit testing
|
||||
- (void)setReservationInterval:(NSTimeInterval)secs;
|
||||
- (NSDictionary *)responses;
|
||||
- (NSUInteger)totalDataSize;
|
||||
@end
|
||||
|
||||
@interface GTMCookieStorage : NSObject <GTMCookieStorageProtocol> {
|
||||
@private
|
||||
// The cookie storage object manages an array holding cookies, but the array
|
||||
// is allocated externally (it may be in a fetcher object or the static
|
||||
// fetcher cookie array.) See the fetcher's setCookieStorageMethod:
|
||||
// for allocation of this object and assignment of its cookies array.
|
||||
NSMutableArray *cookies_;
|
||||
}
|
||||
|
||||
// add all NSHTTPCookies in the supplied array to the storage array,
|
||||
// replacing cookies in the storage array as appropriate
|
||||
// Side effect: removes expired cookies from the storage array
|
||||
- (void)setCookies:(NSArray *)newCookies;
|
||||
|
||||
// retrieve all cookies appropriate for the given URL, considering
|
||||
// domain, path, cookie name, expiration, security setting.
|
||||
// Side effect: removes expired cookies from the storage array
|
||||
- (NSArray *)cookiesForURL:(NSURL *)theURL;
|
||||
|
||||
// return a cookie with the same name, domain, and path as the
|
||||
// given cookie, or else return nil if none found
|
||||
//
|
||||
// Both the cookie being tested and all stored cookies should
|
||||
// be valid (non-nil name, domains, paths)
|
||||
- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie;
|
||||
|
||||
// remove any expired cookies, excluding cookies with nil expirations
|
||||
- (void)removeExpiredCookies;
|
||||
|
||||
- (void)removeAllCookies;
|
||||
|
||||
@end
|
||||
590
OAuth 2/GTMHTTPFetchHistory.m
Executable file
590
OAuth 2/GTMHTTPFetchHistory.m
Executable file
@@ -0,0 +1,590 @@
|
||||
/* Copyright (c) 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//
|
||||
// GTMHTTPFetchHistory.m
|
||||
//
|
||||
|
||||
#define GTMHTTPFETCHHISTORY_DEFINE_GLOBALS 1
|
||||
|
||||
#import "GTMHTTPFetchHistory.h"
|
||||
|
||||
const NSTimeInterval kCachedURLReservationInterval = 60.0; // 1 minute
|
||||
static NSString* const kGTMIfNoneMatchHeader = @"If-None-Match";
|
||||
static NSString* const kGTMETagHeader = @"Etag";
|
||||
|
||||
@implementation GTMCookieStorage
|
||||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
cookies_ = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[cookies_ release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
// add all cookies in the new cookie array to the storage,
|
||||
// replacing stored cookies as appropriate
|
||||
//
|
||||
// Side effect: removes expired cookies from the storage array
|
||||
- (void)setCookies:(NSArray *)newCookies {
|
||||
|
||||
@synchronized(cookies_) {
|
||||
[self removeExpiredCookies];
|
||||
|
||||
for (NSHTTPCookie *newCookie in newCookies) {
|
||||
if ([[newCookie name] length] > 0
|
||||
&& [[newCookie domain] length] > 0
|
||||
&& [[newCookie path] length] > 0) {
|
||||
|
||||
// remove the cookie if it's currently in the array
|
||||
NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie];
|
||||
if (oldCookie) {
|
||||
[cookies_ removeObjectIdenticalTo:oldCookie];
|
||||
}
|
||||
|
||||
// make sure the cookie hasn't already expired
|
||||
NSDate *expiresDate = [newCookie expiresDate];
|
||||
if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) {
|
||||
[cookies_ addObject:newCookie];
|
||||
}
|
||||
|
||||
} else {
|
||||
NSAssert1(NO, @"Cookie incomplete: %@", newCookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)deleteCookie:(NSHTTPCookie *)cookie {
|
||||
@synchronized(cookies_) {
|
||||
NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie];
|
||||
if (foundCookie) {
|
||||
[cookies_ removeObjectIdenticalTo:foundCookie];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// retrieve all cookies appropriate for the given URL, considering
|
||||
// domain, path, cookie name, expiration, security setting.
|
||||
// Side effect: removed expired cookies from the storage array
|
||||
- (NSArray *)cookiesForURL:(NSURL *)theURL {
|
||||
|
||||
NSMutableArray *foundCookies = nil;
|
||||
|
||||
@synchronized(cookies_) {
|
||||
[self removeExpiredCookies];
|
||||
|
||||
// we'll prepend "." to the desired domain, since we want the
|
||||
// actual domain "nytimes.com" to still match the cookie domain
|
||||
// ".nytimes.com" when we check it below with hasSuffix
|
||||
NSString *host = [[theURL host] lowercaseString];
|
||||
NSString *path = [theURL path];
|
||||
NSString *scheme = [theURL scheme];
|
||||
|
||||
NSString *domain = nil;
|
||||
BOOL isLocalhostRetrieval = NO;
|
||||
|
||||
if ([host isEqual:@"localhost"]) {
|
||||
isLocalhostRetrieval = YES;
|
||||
} else {
|
||||
if (host) {
|
||||
domain = [@"." stringByAppendingString:host];
|
||||
}
|
||||
}
|
||||
|
||||
NSUInteger numberOfCookies = [cookies_ count];
|
||||
for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
|
||||
|
||||
NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
|
||||
|
||||
NSString *cookieDomain = [[storedCookie domain] lowercaseString];
|
||||
NSString *cookiePath = [storedCookie path];
|
||||
BOOL cookieIsSecure = [storedCookie isSecure];
|
||||
|
||||
BOOL isDomainOK;
|
||||
|
||||
if (isLocalhostRetrieval) {
|
||||
// prior to 10.5.6, the domain stored into NSHTTPCookies for localhost
|
||||
// is "localhost.local"
|
||||
isDomainOK = [cookieDomain isEqual:@"localhost"]
|
||||
|| [cookieDomain isEqual:@"localhost.local"];
|
||||
} else {
|
||||
isDomainOK = [domain hasSuffix:cookieDomain];
|
||||
}
|
||||
|
||||
BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
|
||||
BOOL isSecureOK = (!cookieIsSecure) || [scheme isEqual:@"https"];
|
||||
|
||||
if (isDomainOK && isPathOK && isSecureOK) {
|
||||
if (foundCookies == nil) {
|
||||
foundCookies = [NSMutableArray arrayWithCapacity:1];
|
||||
}
|
||||
[foundCookies addObject:storedCookie];
|
||||
}
|
||||
}
|
||||
}
|
||||
return foundCookies;
|
||||
}
|
||||
|
||||
// return a cookie from the array with the same name, domain, and path as the
|
||||
// given cookie, or else return nil if none found
|
||||
//
|
||||
// Both the cookie being tested and all cookies in the storage array should
|
||||
// be valid (non-nil name, domains, paths)
|
||||
//
|
||||
// note: this should only be called from inside a @synchronized(cookies_) block
|
||||
- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie {
|
||||
|
||||
NSUInteger numberOfCookies = [cookies_ count];
|
||||
NSString *name = [cookie name];
|
||||
NSString *domain = [cookie domain];
|
||||
NSString *path = [cookie path];
|
||||
|
||||
NSAssert3(name && domain && path, @"Invalid cookie (name:%@ domain:%@ path:%@)",
|
||||
name, domain, path);
|
||||
|
||||
for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
|
||||
|
||||
NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
|
||||
|
||||
if ([[storedCookie name] isEqual:name]
|
||||
&& [[storedCookie domain] isEqual:domain]
|
||||
&& [[storedCookie path] isEqual:path]) {
|
||||
|
||||
return storedCookie;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
// internal routine to remove any expired cookies from the array, excluding
|
||||
// cookies with nil expirations
|
||||
//
|
||||
// note: this should only be called from inside a @synchronized(cookies_) block
|
||||
- (void)removeExpiredCookies {
|
||||
|
||||
// count backwards since we're deleting items from the array
|
||||
for (NSInteger idx = [cookies_ count] - 1; idx >= 0; idx--) {
|
||||
|
||||
NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
|
||||
|
||||
NSDate *expiresDate = [storedCookie expiresDate];
|
||||
if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) {
|
||||
[cookies_ removeObjectAtIndex:idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeAllCookies {
|
||||
@synchronized(cookies_) {
|
||||
[cookies_ removeAllObjects];
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
//
|
||||
// GTMCachedURLResponse
|
||||
//
|
||||
|
||||
@implementation GTMCachedURLResponse
|
||||
|
||||
@synthesize response = response_;
|
||||
@synthesize data = data_;
|
||||
@synthesize reservationDate = reservationDate_;
|
||||
@synthesize useDate = useDate_;
|
||||
|
||||
- (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
response_ = [response retain];
|
||||
data_ = [data retain];
|
||||
useDate_ = [[NSDate alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[response_ release];
|
||||
[data_ release];
|
||||
[useDate_ release];
|
||||
[reservationDate_ release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
NSString *reservationStr = reservationDate_ ?
|
||||
[NSString stringWithFormat:@" resDate:%@", reservationDate_] : @"";
|
||||
|
||||
return [NSString stringWithFormat:@"%@ %p: {bytes:%@ useDate:%@%@}",
|
||||
[self class], self,
|
||||
data_ ? [NSNumber numberWithInt:(int)[data_ length]] : nil,
|
||||
useDate_,
|
||||
reservationStr];
|
||||
}
|
||||
|
||||
- (NSComparisonResult)compareUseDate:(GTMCachedURLResponse *)other {
|
||||
return [useDate_ compare:[other useDate]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
//
|
||||
// GTMURLCache
|
||||
//
|
||||
|
||||
@implementation GTMURLCache
|
||||
|
||||
@dynamic memoryCapacity;
|
||||
|
||||
- (id)init {
|
||||
return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity];
|
||||
}
|
||||
|
||||
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
memoryCapacity_ = totalBytes;
|
||||
|
||||
responses_ = [[NSMutableDictionary alloc] initWithCapacity:5];
|
||||
|
||||
reservationInterval_ = kCachedURLReservationInterval;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[responses_ release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"%@ %p: {responses:%@}",
|
||||
[self class], self, [responses_ allValues]];
|
||||
}
|
||||
|
||||
// setters/getters
|
||||
|
||||
- (void)pruneCacheResponses {
|
||||
// internal routine to remove the least-recently-used responses when the
|
||||
// cache has grown too large
|
||||
if (memoryCapacity_ >= totalDataSize_) return;
|
||||
|
||||
// sort keys by date
|
||||
SEL sel = @selector(compareUseDate:);
|
||||
NSArray *sortedKeys = [responses_ keysSortedByValueUsingSelector:sel];
|
||||
|
||||
// the least-recently-used keys are at the beginning of the sorted array;
|
||||
// remove those (except ones still reserved) until the total data size is
|
||||
// reduced sufficiently
|
||||
for (NSURL *key in sortedKeys) {
|
||||
GTMCachedURLResponse *response = [responses_ objectForKey:key];
|
||||
|
||||
NSDate *resDate = [response reservationDate];
|
||||
BOOL isResponseReserved = (resDate != nil)
|
||||
&& ([resDate timeIntervalSinceNow] > -reservationInterval_);
|
||||
|
||||
if (!isResponseReserved) {
|
||||
// we can remove this response from the cache
|
||||
NSUInteger storedSize = [[response data] length];
|
||||
totalDataSize_ -= storedSize;
|
||||
[responses_ removeObjectForKey:key];
|
||||
}
|
||||
|
||||
// if we've removed enough response data, then we're done
|
||||
if (memoryCapacity_ >= totalDataSize_) break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse
|
||||
forRequest:(NSURLRequest *)request {
|
||||
@synchronized(self) {
|
||||
// remove any previous entry for this request
|
||||
[self removeCachedResponseForRequest:request];
|
||||
|
||||
// cache this one only if it's not bigger than our cache
|
||||
NSUInteger storedSize = [[cachedResponse data] length];
|
||||
if (storedSize < memoryCapacity_) {
|
||||
|
||||
NSURL *key = [request URL];
|
||||
[responses_ setObject:cachedResponse forKey:key];
|
||||
totalDataSize_ += storedSize;
|
||||
|
||||
[self pruneCacheResponses];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
|
||||
GTMCachedURLResponse *response;
|
||||
|
||||
@synchronized(self) {
|
||||
NSURL *key = [request URL];
|
||||
response = [[[responses_ objectForKey:key] retain] autorelease];
|
||||
|
||||
// touch the date to indicate this was recently retrieved
|
||||
[response setUseDate:[NSDate date]];
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
- (void)removeCachedResponseForRequest:(NSURLRequest *)request {
|
||||
@synchronized(self) {
|
||||
NSURL *key = [request URL];
|
||||
totalDataSize_ -= [[[responses_ objectForKey:key] data] length];
|
||||
[responses_ removeObjectForKey:key];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeAllCachedResponses {
|
||||
@synchronized(self) {
|
||||
[responses_ removeAllObjects];
|
||||
totalDataSize_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSUInteger)memoryCapacity {
|
||||
return memoryCapacity_;
|
||||
}
|
||||
|
||||
- (void)setMemoryCapacity:(NSUInteger)totalBytes {
|
||||
@synchronized(self) {
|
||||
BOOL didShrink = (totalBytes < memoryCapacity_);
|
||||
memoryCapacity_ = totalBytes;
|
||||
|
||||
if (didShrink) {
|
||||
[self pruneCacheResponses];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// methods for unit testing
|
||||
- (void)setReservationInterval:(NSTimeInterval)secs {
|
||||
reservationInterval_ = secs;
|
||||
}
|
||||
|
||||
- (NSDictionary *)responses {
|
||||
return responses_;
|
||||
}
|
||||
|
||||
- (NSUInteger)totalDataSize {
|
||||
return totalDataSize_;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
//
|
||||
// GTMHTTPFetchHistory
|
||||
//
|
||||
|
||||
@interface GTMHTTPFetchHistory ()
|
||||
- (NSString *)cachedETagForRequest:(NSURLRequest *)request;
|
||||
- (void)removeCachedDataForRequest:(NSURLRequest *)request;
|
||||
@end
|
||||
|
||||
@implementation GTMHTTPFetchHistory
|
||||
|
||||
@synthesize cookieStorage = cookieStorage_;
|
||||
|
||||
@dynamic shouldRememberETags;
|
||||
@dynamic shouldCacheETaggedData;
|
||||
@dynamic memoryCapacity;
|
||||
|
||||
- (id)init {
|
||||
return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity
|
||||
shouldCacheETaggedData:NO];
|
||||
}
|
||||
|
||||
- (id)initWithMemoryCapacity:(NSUInteger)totalBytes
|
||||
shouldCacheETaggedData:(BOOL)shouldCacheETaggedData {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
etaggedDataCache_ = [[GTMURLCache alloc] initWithMemoryCapacity:totalBytes];
|
||||
shouldRememberETags_ = shouldCacheETaggedData;
|
||||
shouldCacheETaggedData_ = shouldCacheETaggedData;
|
||||
cookieStorage_ = [[GTMCookieStorage alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[etaggedDataCache_ release];
|
||||
[cookieStorage_ release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet {
|
||||
if ([self shouldRememberETags]) {
|
||||
// If this URL is in the history, and no ETag has been set, then
|
||||
// set the ETag header field
|
||||
|
||||
// if we have a history, we're tracking across fetches, so we don't
|
||||
// want to pull results from any other cache
|
||||
[request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
|
||||
|
||||
if (isHTTPGet) {
|
||||
// we'll only add an ETag if there's no ETag specified in the user's
|
||||
// request
|
||||
NSString *specifiedETag = [request valueForHTTPHeaderField:kGTMIfNoneMatchHeader];
|
||||
if (specifiedETag == nil) {
|
||||
// no ETag: extract the previous ETag for this request from the
|
||||
// fetch history, and add it to the request
|
||||
NSString *cachedETag = [self cachedETagForRequest:request];
|
||||
|
||||
if (cachedETag != nil) {
|
||||
[request addValue:cachedETag forHTTPHeaderField:kGTMIfNoneMatchHeader];
|
||||
}
|
||||
} else {
|
||||
// has an ETag: remove any stored response in the fetch history
|
||||
// for this request, as the If-None-Match header could lead to
|
||||
// a 304 Not Modified, and we want that error delivered to the
|
||||
// user since they explicitly specified the ETag
|
||||
[self removeCachedDataForRequest:request];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateFetchHistoryWithRequest:(NSURLRequest *)request
|
||||
response:(NSURLResponse *)response
|
||||
downloadedData:(NSData *)downloadedData {
|
||||
if (![self shouldRememberETags]) return;
|
||||
|
||||
if (![response respondsToSelector:@selector(allHeaderFields)]) return;
|
||||
|
||||
NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
|
||||
if (statusCode != kGTMHTTPFetcherStatusNotModified) {
|
||||
// save this ETag string for successful results (<300)
|
||||
// If there's no last modified string, clear the dictionary
|
||||
// entry for this URL. Also cache or delete the data, if appropriate
|
||||
// (when etaggedDataCache is non-nil.)
|
||||
NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
|
||||
NSString* etag = [headers objectForKey:kGTMETagHeader];
|
||||
|
||||
if (etag != nil && statusCode < 300) {
|
||||
|
||||
// we want to cache responses for the headers, even if the client
|
||||
// doesn't want the response body data caches
|
||||
NSData *dataToStore = shouldCacheETaggedData_ ? downloadedData : nil;
|
||||
|
||||
GTMCachedURLResponse *cachedResponse;
|
||||
cachedResponse = [[[GTMCachedURLResponse alloc] initWithResponse:response
|
||||
data:dataToStore] autorelease];
|
||||
[etaggedDataCache_ storeCachedResponse:cachedResponse
|
||||
forRequest:request];
|
||||
} else {
|
||||
[etaggedDataCache_ removeCachedResponseForRequest:request];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)cachedETagForRequest:(NSURLRequest *)request {
|
||||
GTMCachedURLResponse *cachedResponse;
|
||||
cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
|
||||
|
||||
NSURLResponse *response = [cachedResponse response];
|
||||
NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
|
||||
NSString *cachedETag = [headers objectForKey:kGTMETagHeader];
|
||||
if (cachedETag) {
|
||||
// since the request having an ETag implies this request is about
|
||||
// to be fetched again, reserve the cached response to ensure that
|
||||
// that it will be around at least until the fetch completes
|
||||
//
|
||||
// when the fetch completes, either the cached response will be replaced
|
||||
// with a new response, or the cachedDataForRequest: method below will
|
||||
// clear the reservation
|
||||
[cachedResponse setReservationDate:[NSDate date]];
|
||||
}
|
||||
return cachedETag;
|
||||
}
|
||||
|
||||
- (NSData *)cachedDataForRequest:(NSURLRequest *)request {
|
||||
GTMCachedURLResponse *cachedResponse;
|
||||
cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
|
||||
|
||||
NSData *cachedData = [cachedResponse data];
|
||||
|
||||
// since the data for this cached request is being obtained from the cache,
|
||||
// we can clear the reservation as the fetch has completed
|
||||
[cachedResponse setReservationDate:nil];
|
||||
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
- (void)removeCachedDataForRequest:(NSURLRequest *)request {
|
||||
[etaggedDataCache_ removeCachedResponseForRequest:request];
|
||||
}
|
||||
|
||||
- (void)clearETaggedDataCache {
|
||||
[etaggedDataCache_ removeAllCachedResponses];
|
||||
}
|
||||
|
||||
- (void)clearHistory {
|
||||
[self clearETaggedDataCache];
|
||||
[cookieStorage_ removeAllCookies];
|
||||
}
|
||||
|
||||
- (void)removeAllCookies {
|
||||
[cookieStorage_ removeAllCookies];
|
||||
}
|
||||
|
||||
- (BOOL)shouldRememberETags {
|
||||
return shouldRememberETags_;
|
||||
}
|
||||
|
||||
- (void)setShouldRememberETags:(BOOL)flag {
|
||||
BOOL wasRemembering = shouldRememberETags_;
|
||||
shouldRememberETags_ = flag;
|
||||
|
||||
if (wasRemembering && !flag) {
|
||||
// free up the cache memory
|
||||
[self clearETaggedDataCache];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)shouldCacheETaggedData {
|
||||
return shouldCacheETaggedData_;
|
||||
}
|
||||
|
||||
- (void)setShouldCacheETaggedData:(BOOL)flag {
|
||||
BOOL wasCaching = shouldCacheETaggedData_;
|
||||
shouldCacheETaggedData_ = flag;
|
||||
|
||||
if (flag) {
|
||||
self.shouldRememberETags = YES;
|
||||
}
|
||||
|
||||
if (wasCaching && !flag) {
|
||||
// users expect turning off caching to free up the cache memory
|
||||
[self clearETaggedDataCache];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSUInteger)memoryCapacity {
|
||||
return [etaggedDataCache_ memoryCapacity];
|
||||
}
|
||||
|
||||
- (void)setMemoryCapacity:(NSUInteger)totalBytes {
|
||||
[etaggedDataCache_ setMemoryCapacity:totalBytes];
|
||||
}
|
||||
|
||||
@end
|
||||
704
OAuth 2/GTMHTTPFetcher.h
Executable file
704
OAuth 2/GTMHTTPFetcher.h
Executable file
@@ -0,0 +1,704 @@
|
||||
/* Copyright (c) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//
|
||||
// GTMHTTPFetcher.h
|
||||
//
|
||||
|
||||
// This is essentially a wrapper around NSURLConnection for POSTs and GETs.
|
||||
// If setPostData: is called, then POST is assumed.
|
||||
//
|
||||
// When would you use this instead of NSURLConnection?
|
||||
//
|
||||
// - When you just want the result from a GET, POST, or PUT
|
||||
// - When you want the "standard" behavior for connections (redirection handling
|
||||
// an so on)
|
||||
// - When you want automatic retry on failures
|
||||
// - When you want to avoid cookie collisions with Safari and other applications
|
||||
// - When you are fetching resources with ETags and want to avoid the overhead
|
||||
// of repeated fetches of unchanged data
|
||||
// - When you need to set a credential for the http operation
|
||||
//
|
||||
// This is assumed to be a one-shot fetch request; don't reuse the object
|
||||
// for a second fetch.
|
||||
//
|
||||
// The fetcher may be created auto-released, in which case it will release
|
||||
// itself after the fetch completion callback. The fetcher is implicitly
|
||||
// retained as long as a connection is pending.
|
||||
//
|
||||
// But if you may need to cancel the fetcher, retain it and have the delegate
|
||||
// release the fetcher in the callbacks.
|
||||
//
|
||||
// Sample usage:
|
||||
//
|
||||
// NSURLRequest *request = [NSURLRequest requestWithURL:myURL];
|
||||
// GTMHTTPFetcher* myFetcher = [GTMHTTPFetcher fetcherWithRequest:request];
|
||||
//
|
||||
// // optional upload body data
|
||||
// [myFetcher setPostData:[postString dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
//
|
||||
// [myFetcher beginFetchWithDelegate:self
|
||||
// didFinishSelector:@selector(myFetcher:finishedWithData:error:)];
|
||||
//
|
||||
// Upon fetch completion, the callback selector is invoked; it should have
|
||||
// this signature (you can use any callback method name you want so long as
|
||||
// the signature matches this):
|
||||
//
|
||||
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)retrievedData error:(NSError *)error;
|
||||
//
|
||||
// The block callback version looks like:
|
||||
//
|
||||
// [myFetcher beginFetchWithCompletionHandler:^(NSData *retrievedData, NSError *error) {
|
||||
// if (error != nil) {
|
||||
// // status code or network error
|
||||
// } else {
|
||||
// // succeeded
|
||||
// }
|
||||
// }];
|
||||
|
||||
//
|
||||
// NOTE: Fetches may retrieve data from the server even though the server
|
||||
// returned an error. The failure selector is called when the server
|
||||
// status is >= 300, with an NSError having domain
|
||||
// kGTMHTTPFetcherStatusDomain and code set to the server status.
|
||||
//
|
||||
// Status codes are at <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html>
|
||||
//
|
||||
//
|
||||
// Downloading to disk:
|
||||
//
|
||||
// To have downloaded data saved directly to disk, specify either a path for the
|
||||
// downloadPath property, or a file handle for the downloadFileHandle property.
|
||||
// When downloading to disk, callbacks will be passed a nil for the NSData*
|
||||
// arguments.
|
||||
//
|
||||
//
|
||||
// HTTP methods and headers:
|
||||
//
|
||||
// Alternative HTTP methods, like PUT, and custom headers can be specified by
|
||||
// creating the fetcher with an appropriate NSMutableURLRequest
|
||||
//
|
||||
//
|
||||
// Proxies:
|
||||
//
|
||||
// Proxy handling is invisible so long as the system has a valid credential in
|
||||
// the keychain, which is normally true (else most NSURL-based apps would have
|
||||
// difficulty.) But when there is a proxy authetication error, the the fetcher
|
||||
// will call the failedWithError: method with the NSURLChallenge in the error's
|
||||
// userInfo. The error method can get the challenge info like this:
|
||||
//
|
||||
// NSURLAuthenticationChallenge *challenge
|
||||
// = [[error userInfo] objectForKey:kGTMHTTPFetcherErrorChallengeKey];
|
||||
// BOOL isProxyChallenge = [[challenge protectionSpace] isProxy];
|
||||
//
|
||||
// If a proxy error occurs, you can ask the user for the proxy username/password
|
||||
// and call fetcher's setProxyCredential: to provide those for the
|
||||
// next attempt to fetch.
|
||||
//
|
||||
//
|
||||
// Cookies:
|
||||
//
|
||||
// There are three supported mechanisms for remembering cookies between fetches.
|
||||
//
|
||||
// By default, GTMHTTPFetcher uses a mutable array held statically to track
|
||||
// cookies for all instantiated fetchers. This avoids server cookies being set
|
||||
// by servers for the application from interfering with Safari cookie settings,
|
||||
// and vice versa. The fetcher cookies are lost when the application quits.
|
||||
//
|
||||
// To rely instead on WebKit's global NSHTTPCookieStorage, call
|
||||
// setCookieStorageMethod: with kGTMHTTPFetcherCookieStorageMethodSystemDefault.
|
||||
//
|
||||
// If the fetcher is created from a GTMHTTPFetcherService object
|
||||
// then the cookie storage mechanism is set to use the cookie storage in the
|
||||
// service object rather than the static storage.
|
||||
//
|
||||
//
|
||||
// Fetching for periodic checks:
|
||||
//
|
||||
// The fetcher object tracks ETag headers from responses and
|
||||
// provide an "If-None-Match" header. This allows the server to save
|
||||
// bandwidth by providing a status message instead of repeated response
|
||||
// data.
|
||||
//
|
||||
// To get this behavior, create the fetcher from an GTMHTTPFetcherService object
|
||||
// and look for a fetch callback error with code 304
|
||||
// (kGTMHTTPFetcherStatusNotModified) like this:
|
||||
//
|
||||
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error {
|
||||
// if ([error code] == kGTMHTTPFetcherStatusNotModified) {
|
||||
// // |data| is empty; use the data from the previous finishedWithData: for this URL
|
||||
// } else {
|
||||
// // handle other server status code
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// Monitoring received data
|
||||
//
|
||||
// The optional received data selector can be set with setReceivedDataSelector:
|
||||
// and should have the signature
|
||||
//
|
||||
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher receivedData:(NSData *)dataReceivedSoFar;
|
||||
//
|
||||
// The number bytes received so far is available as [fetcher downloadedLength].
|
||||
// This number may go down if a redirect causes the download to begin again from
|
||||
// a new server.
|
||||
//
|
||||
// If supplied by the server, the anticipated total download size is available
|
||||
// as [[myFetcher response] expectedContentLength] (and may be -1 for unknown
|
||||
// download sizes.)
|
||||
//
|
||||
//
|
||||
// Automatic retrying of fetches
|
||||
//
|
||||
// The fetcher can optionally create a timer and reattempt certain kinds of
|
||||
// fetch failures (status codes 408, request timeout; 503, service unavailable;
|
||||
// 504, gateway timeout; networking errors NSURLErrorTimedOut and
|
||||
// NSURLErrorNetworkConnectionLost.) The user may set a retry selector to
|
||||
// customize the type of errors which will be retried.
|
||||
//
|
||||
// Retries are done in an exponential-backoff fashion (that is, after 1 second,
|
||||
// 2, 4, 8, and so on.)
|
||||
//
|
||||
// Enabling automatic retries looks like this:
|
||||
// [myFetcher setRetryEnabled:YES];
|
||||
//
|
||||
// With retries enabled, the success or failure callbacks are called only
|
||||
// when no more retries will be attempted. Calling the fetcher's stopFetching
|
||||
// method will terminate the retry timer, without the finished or failure
|
||||
// selectors being invoked.
|
||||
//
|
||||
// Optionally, the client may set the maximum retry interval:
|
||||
// [myFetcher setMaxRetryInterval:60.0]; // in seconds; default is 60 seconds
|
||||
// // for downloads, 600 for uploads
|
||||
//
|
||||
// Also optionally, the client may provide a callback selector to determine
|
||||
// if a status code or other error should be retried.
|
||||
// [myFetcher setRetrySelector:@selector(myFetcher:willRetry:forError:)];
|
||||
//
|
||||
// If set, the retry selector should have the signature:
|
||||
// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error
|
||||
// and return YES to set the retry timer or NO to fail without additional
|
||||
// fetch attempts.
|
||||
//
|
||||
// The retry method may return the |suggestedWillRetry| argument to get the
|
||||
// default retry behavior. Server status codes are present in the
|
||||
// error argument, and have the domain kGTMHTTPFetcherStatusDomain. The
|
||||
// user's method may look something like this:
|
||||
//
|
||||
// -(BOOL)myFetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error {
|
||||
//
|
||||
// // perhaps examine [error domain] and [error code], or [fetcher retryCount]
|
||||
// //
|
||||
// // return YES to start the retry timer, NO to proceed to the failure
|
||||
// // callback, or |suggestedWillRetry| to get default behavior for the
|
||||
// // current error domain and code values.
|
||||
// return suggestedWillRetry;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#if defined(GTL_TARGET_NAMESPACE)
|
||||
// we're using target namespace macros
|
||||
#import "GTLDefines.h"
|
||||
#elif defined(GDATA_TARGET_NAMESPACE)
|
||||
#import "GDataDefines.h"
|
||||
#else
|
||||
#if TARGET_OS_IPHONE
|
||||
#ifndef GTM_FOUNDATION_ONLY
|
||||
#define GTM_FOUNDATION_ONLY 1
|
||||
#endif
|
||||
#ifndef GTM_IPHONE
|
||||
#define GTM_IPHONE 1
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 40000)
|
||||
#define GTM_BACKGROUND_FETCHING 1
|
||||
#endif
|
||||
|
||||
#undef _EXTERN
|
||||
#undef _INITIALIZE_AS
|
||||
#ifdef GTMHTTPFETCHER_DEFINE_GLOBALS
|
||||
#define _EXTERN
|
||||
#define _INITIALIZE_AS(x) =x
|
||||
#else
|
||||
#if defined(__cplusplus)
|
||||
#define _EXTERN extern "C"
|
||||
#else
|
||||
#define _EXTERN extern
|
||||
#endif
|
||||
#define _INITIALIZE_AS(x)
|
||||
#endif
|
||||
|
||||
// notifications
|
||||
//
|
||||
// fetch started and stopped, and fetch retry delay started and stopped
|
||||
_EXTERN NSString* const kGTMHTTPFetcherStartedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherStartedNotification");
|
||||
_EXTERN NSString* const kGTMHTTPFetcherStoppedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherStoppedNotification");
|
||||
_EXTERN NSString* const kGTMHTTPFetcherRetryDelayStartedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherRetryDelayStartedNotification");
|
||||
_EXTERN NSString* const kGTMHTTPFetcherRetryDelayStoppedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherRetryDelayStoppedNotification");
|
||||
|
||||
// callback constants
|
||||
_EXTERN NSString* const kGTMHTTPFetcherErrorDomain _INITIALIZE_AS(@"com.google.GTMHTTPFetcher");
|
||||
_EXTERN NSString* const kGTMHTTPFetcherStatusDomain _INITIALIZE_AS(@"com.google.HTTPStatus");
|
||||
_EXTERN NSString* const kGTMHTTPFetcherErrorChallengeKey _INITIALIZE_AS(@"challenge");
|
||||
_EXTERN NSString* const kGTMHTTPFetcherStatusDataKey _INITIALIZE_AS(@"data"); // data returned with a kGTMHTTPFetcherStatusDomain error
|
||||
|
||||
enum {
|
||||
kGTMHTTPFetcherErrorDownloadFailed = -1,
|
||||
kGTMHTTPFetcherErrorAuthenticationChallengeFailed = -2,
|
||||
kGTMHTTPFetcherErrorChunkUploadFailed = -3,
|
||||
kGTMHTTPFetcherErrorFileHandleException = -4,
|
||||
kGTMHTTPFetcherErrorBackgroundExpiration = -6,
|
||||
|
||||
// The code kGTMHTTPFetcherErrorAuthorizationFailed (-5) has been removed;
|
||||
// look for status 401 instead.
|
||||
|
||||
kGTMHTTPFetcherStatusNotModified = 304,
|
||||
kGTMHTTPFetcherStatusBadRequest = 400,
|
||||
kGTMHTTPFetcherStatusUnauthorized = 401,
|
||||
kGTMHTTPFetcherStatusForbidden = 403,
|
||||
kGTMHTTPFetcherStatusPreconditionFailed = 412
|
||||
};
|
||||
|
||||
// cookie storage methods
|
||||
enum {
|
||||
kGTMHTTPFetcherCookieStorageMethodStatic = 0,
|
||||
kGTMHTTPFetcherCookieStorageMethodFetchHistory = 1,
|
||||
kGTMHTTPFetcherCookieStorageMethodSystemDefault = 2,
|
||||
kGTMHTTPFetcherCookieStorageMethodNone = 3
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void GTMAssertSelectorNilOrImplementedWithArgs(id obj, SEL sel, ...);
|
||||
|
||||
// Utility functions for applications self-identifying to servers via a
|
||||
// user-agent header
|
||||
|
||||
// Make a proper app name without whitespace from the given string, removing
|
||||
// whitespace and other characters that may be special parsed marks of
|
||||
// the full user-agent string.
|
||||
NSString *GTMCleanedUserAgentString(NSString *str);
|
||||
|
||||
// Make an identifier like "MacOSX/10.7.1" or "iPod_Touch/4.1"
|
||||
NSString *GTMSystemVersionString(void);
|
||||
|
||||
// Make a generic name and version for the current application, like
|
||||
// com.example.MyApp/1.2.3 relying on the bundle identifier and the
|
||||
// CFBundleShortVersionString or CFBundleVersion. If no bundle ID
|
||||
// is available, the process name preceded by "proc_" is used.
|
||||
NSString *GTMApplicationIdentifier(NSBundle *bundle);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
@class GTMHTTPFetcher;
|
||||
|
||||
@protocol GTMCookieStorageProtocol <NSObject>
|
||||
// This protocol allows us to call into the service without requiring
|
||||
// GTMCookieStorage sources in this project
|
||||
//
|
||||
// The public interface for cookie handling is the GTMCookieStorage class,
|
||||
// accessible from a fetcher service object's fetchHistory or from the fetcher's
|
||||
// +staticCookieStorage method.
|
||||
- (NSArray *)cookiesForURL:(NSURL *)theURL;
|
||||
- (void)setCookies:(NSArray *)newCookies;
|
||||
@end
|
||||
|
||||
@protocol GTMHTTPFetchHistoryProtocol <NSObject>
|
||||
// This protocol allows us to call the fetch history object without requiring
|
||||
// GTMHTTPFetchHistory sources in this project
|
||||
- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet;
|
||||
- (BOOL)shouldCacheETaggedData;
|
||||
- (NSData *)cachedDataForRequest:(NSURLRequest *)request;
|
||||
- (id <GTMCookieStorageProtocol>)cookieStorage;
|
||||
- (void)updateFetchHistoryWithRequest:(NSURLRequest *)request
|
||||
response:(NSURLResponse *)response
|
||||
downloadedData:(NSData *)downloadedData;
|
||||
- (void)removeCachedDataForRequest:(NSURLRequest *)request;
|
||||
@end
|
||||
|
||||
@protocol GTMHTTPFetcherServiceProtocol <NSObject>
|
||||
// This protocol allows us to call into the service without requiring
|
||||
// GTMHTTPFetcherService sources in this project
|
||||
- (BOOL)fetcherShouldBeginFetching:(GTMHTTPFetcher *)fetcher;
|
||||
- (void)fetcherDidStop:(GTMHTTPFetcher *)fetcher;
|
||||
|
||||
- (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request;
|
||||
- (BOOL)isDelayingFetcher:(GTMHTTPFetcher *)fetcher;
|
||||
@end
|
||||
|
||||
@protocol GTMFetcherAuthorizationProtocol <NSObject>
|
||||
@required
|
||||
// This protocol allows us to call the authorizer without requiring its sources
|
||||
// in this project
|
||||
- (void)authorizeRequest:(NSMutableURLRequest *)request
|
||||
delegate:(id)delegate
|
||||
didFinishSelector:(SEL)sel;
|
||||
|
||||
- (void)stopAuthorization;
|
||||
|
||||
- (BOOL)isAuthorizingRequest:(NSURLRequest *)request;
|
||||
|
||||
- (BOOL)isAuthorizedRequest:(NSURLRequest *)request;
|
||||
|
||||
- (NSString *)userEmail;
|
||||
|
||||
@optional
|
||||
@property (assign) id <GTMHTTPFetcherServiceProtocol> fetcherService; // WEAK
|
||||
|
||||
- (BOOL)primeForRefresh;
|
||||
@end
|
||||
|
||||
// GTMHTTPFetcher objects are used for async retrieval of an http get or post
|
||||
//
|
||||
// See additional comments at the beginning of this file
|
||||
@interface GTMHTTPFetcher : NSObject {
|
||||
@protected
|
||||
NSMutableURLRequest *request_;
|
||||
NSURLConnection *connection_;
|
||||
NSMutableData *downloadedData_;
|
||||
NSString *downloadPath_;
|
||||
NSString *temporaryDownloadPath_;
|
||||
NSFileHandle *downloadFileHandle_;
|
||||
unsigned long long downloadedLength_;
|
||||
NSURLCredential *credential_; // username & password
|
||||
NSURLCredential *proxyCredential_; // credential supplied to proxy servers
|
||||
NSData *postData_;
|
||||
NSInputStream *postStream_;
|
||||
NSMutableData *loggedStreamData_;
|
||||
NSURLResponse *response_; // set in connection:didReceiveResponse:
|
||||
id delegate_;
|
||||
SEL finishedSel_; // should by implemented by delegate
|
||||
SEL sentDataSel_; // optional, set with setSentDataSelector
|
||||
SEL receivedDataSel_; // optional, set with setReceivedDataSelector
|
||||
#if NS_BLOCKS_AVAILABLE
|
||||
void (^completionBlock_)(NSData *, NSError *);
|
||||
void (^receivedDataBlock_)(NSData *);
|
||||
void (^sentDataBlock_)(NSInteger, NSInteger, NSInteger);
|
||||
BOOL (^retryBlock_)(BOOL, NSError *);
|
||||
#elif !__LP64__
|
||||
// placeholders: for 32-bit builds, keep the size of the object's ivar section
|
||||
// the same with and without blocks
|
||||
id completionPlaceholder_;
|
||||
id receivedDataPlaceholder_;
|
||||
id sentDataPlaceholder_;
|
||||
id retryPlaceholder_;
|
||||
#endif
|
||||
BOOL hasConnectionEnded_; // set if the connection need not be cancelled
|
||||
BOOL isCancellingChallenge_; // set only when cancelling an auth challenge
|
||||
BOOL isStopNotificationNeeded_; // set when start notification has been sent
|
||||
BOOL shouldFetchInBackground_;
|
||||
#if GTM_BACKGROUND_FETCHING
|
||||
NSUInteger backgroundTaskIdentifer_; // UIBackgroundTaskIdentifier
|
||||
#endif
|
||||
id userData_; // retained, if set by caller
|
||||
NSMutableDictionary *properties_; // more data retained for caller
|
||||
NSArray *runLoopModes_; // optional, for 10.5 and later
|
||||
id <GTMHTTPFetchHistoryProtocol> fetchHistory_; // if supplied by the caller, used for Last-Modified-Since checks and cookies
|
||||
NSInteger cookieStorageMethod_; // constant from above
|
||||
id <GTMCookieStorageProtocol> cookieStorage_;
|
||||
|
||||
id <GTMFetcherAuthorizationProtocol> authorizer_;
|
||||
|
||||
// the service object that created and monitors this fetcher, if any
|
||||
id <GTMHTTPFetcherServiceProtocol> service_;
|
||||
NSString *serviceHost_;
|
||||
NSInteger servicePriority_;
|
||||
NSThread *thread_;
|
||||
|
||||
BOOL isRetryEnabled_; // user wants auto-retry
|
||||
SEL retrySel_; // optional; set with setRetrySelector
|
||||
NSTimer *retryTimer_;
|
||||
NSUInteger retryCount_;
|
||||
NSTimeInterval maxRetryInterval_; // default 600 seconds
|
||||
NSTimeInterval minRetryInterval_; // random between 1 and 2 seconds
|
||||
NSTimeInterval retryFactor_; // default interval multiplier is 2
|
||||
NSTimeInterval lastRetryInterval_;
|
||||
BOOL hasAttemptedAuthRefresh_;
|
||||
|
||||
NSString *comment_; // comment for log
|
||||
NSString *log_;
|
||||
}
|
||||
|
||||
// Create a fetcher
|
||||
//
|
||||
// fetcherWithRequest will return an autoreleased fetcher, but if
|
||||
// the connection is successfully created, the connection should retain the
|
||||
// fetcher for the life of the connection as well. So the caller doesn't have
|
||||
// to retain the fetcher explicitly unless they want to be able to cancel it.
|
||||
+ (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request;
|
||||
|
||||
// Convenience methods that make a request, like +fetcherWithRequest
|
||||
+ (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL;
|
||||
+ (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString;
|
||||
|
||||
// Designated initializer
|
||||
- (id)initWithRequest:(NSURLRequest *)request;
|
||||
|
||||
// Fetcher request
|
||||
//
|
||||
// The underlying request is mutable and may be modified by the caller
|
||||
@property (retain) NSMutableURLRequest *mutableRequest;
|
||||
|
||||
// Setting the credential is optional; it is used if the connection receives
|
||||
// an authentication challenge
|
||||
@property (retain) NSURLCredential *credential;
|
||||
|
||||
// Setting the proxy credential is optional; it is used if the connection
|
||||
// receives an authentication challenge from a proxy
|
||||
@property (retain) NSURLCredential *proxyCredential;
|
||||
|
||||
// If post data or stream is not set, then a GET retrieval method is assumed
|
||||
@property (retain) NSData *postData;
|
||||
@property (retain) NSInputStream *postStream;
|
||||
|
||||
// The default cookie storage method is kGTMHTTPFetcherCookieStorageMethodStatic
|
||||
// without a fetch history set, and kGTMHTTPFetcherCookieStorageMethodFetchHistory
|
||||
// with a fetch history set
|
||||
//
|
||||
// Applications needing control of cookies across a sequence of fetches should
|
||||
// create fetchers from a GTMHTTPFetcherService object (which encapsulates
|
||||
// fetch history) for a well-defined cookie store
|
||||
@property (assign) NSInteger cookieStorageMethod;
|
||||
|
||||
+ (id <GTMCookieStorageProtocol>)staticCookieStorage;
|
||||
|
||||
// Object to add authorization to the request, if needed
|
||||
@property (retain) id <GTMFetcherAuthorizationProtocol> authorizer;
|
||||
|
||||
// The service object that created and monitors this fetcher, if any
|
||||
@property (retain) id <GTMHTTPFetcherServiceProtocol> service;
|
||||
|
||||
// The host, if any, used to classify this fetcher in the fetcher service
|
||||
@property (copy) NSString *serviceHost;
|
||||
|
||||
// The priority, if any, used for starting fetchers in the fetcher service
|
||||
//
|
||||
// Lower values are higher priority; the default is 0, and values may
|
||||
// be negative or positive. This priority affects only the start order of
|
||||
// fetchers that are being delayed by a fetcher service.
|
||||
@property (assign) NSInteger servicePriority;
|
||||
|
||||
// The thread used to run this fetcher in the fetcher service
|
||||
@property (retain) NSThread *thread;
|
||||
|
||||
// The delegate is retained during the connection
|
||||
@property (retain) id delegate;
|
||||
|
||||
// On iOS 4 and later, the fetch may optionally continue while the app is in the
|
||||
// background until finished or stopped by OS expiration
|
||||
//
|
||||
// The default value is NO
|
||||
//
|
||||
// For Mac OS X, background fetches are always supported, and this property
|
||||
// is ignored
|
||||
@property (assign) BOOL shouldFetchInBackground;
|
||||
|
||||
// The delegate's optional sentData selector may be used to monitor upload
|
||||
// progress. It should have a signature like:
|
||||
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher
|
||||
// didSendBytes:(NSInteger)bytesSent
|
||||
// totalBytesSent:(NSInteger)totalBytesSent
|
||||
// totalBytesExpectedToSend:(NSInteger)totalBytesExpectedToSend;
|
||||
//
|
||||
// +doesSupportSentDataCallback indicates if this delegate method is supported
|
||||
+ (BOOL)doesSupportSentDataCallback;
|
||||
|
||||
@property (assign) SEL sentDataSelector;
|
||||
|
||||
// The delegate's optional receivedData selector may be used to monitor download
|
||||
// progress. It should have a signature like:
|
||||
// - (void)myFetcher:(GTMHTTPFetcher *)fetcher
|
||||
// receivedData:(NSData *)dataReceivedSoFar;
|
||||
//
|
||||
// The dataReceived argument will be nil when downloading to a path or to a
|
||||
// file handle.
|
||||
//
|
||||
// Applications should not use this method to accumulate the received data;
|
||||
// the callback method or block supplied to the beginFetch call will have
|
||||
// the complete NSData received.
|
||||
@property (assign) SEL receivedDataSelector;
|
||||
|
||||
#if NS_BLOCKS_AVAILABLE
|
||||
// The full interface to the block is provided rather than just a typedef for
|
||||
// its parameter list in order to get more useful code completion in the Xcode
|
||||
// editor
|
||||
@property (copy) void (^sentDataBlock)(NSInteger bytesSent, NSInteger totalBytesSent, NSInteger bytesExpectedToSend);
|
||||
|
||||
// The dataReceived argument will be nil when downloading to a path or to
|
||||
// a file handle
|
||||
@property (copy) void (^receivedDataBlock)(NSData *dataReceivedSoFar);
|
||||
#endif
|
||||
|
||||
// retrying; see comments at the top of the file. Calling
|
||||
// setRetryEnabled(YES) resets the min and max retry intervals.
|
||||
@property (assign, getter=isRetryEnabled) BOOL retryEnabled;
|
||||
|
||||
// Retry selector or block is optional for retries.
|
||||
//
|
||||
// If present, it should have the signature:
|
||||
// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error
|
||||
// and return YES to cause a retry. See comments at the top of this file.
|
||||
@property (assign) SEL retrySelector;
|
||||
|
||||
#if NS_BLOCKS_AVAILABLE
|
||||
@property (copy) BOOL (^retryBlock)(BOOL suggestedWillRetry, NSError *error);
|
||||
#endif
|
||||
|
||||
// Retry intervals must be strictly less than maxRetryInterval, else
|
||||
// they will be limited to maxRetryInterval and no further retries will
|
||||
// be attempted. Setting maxRetryInterval to 0.0 will reset it to the
|
||||
// default value, 600 seconds.
|
||||
|
||||
@property (assign) NSTimeInterval maxRetryInterval;
|
||||
|
||||
// Starting retry interval. Setting minRetryInterval to 0.0 will reset it
|
||||
// to a random value between 1.0 and 2.0 seconds. Clients should normally not
|
||||
// call this except for unit testing.
|
||||
@property (assign) NSTimeInterval minRetryInterval;
|
||||
|
||||
// Multiplier used to increase the interval between retries, typically 2.0.
|
||||
// Clients should not need to call this.
|
||||
@property (assign) double retryFactor;
|
||||
|
||||
// Number of retries attempted
|
||||
@property (readonly) NSUInteger retryCount;
|
||||
|
||||
// interval delay to precede next retry
|
||||
@property (readonly) NSTimeInterval nextRetryInterval;
|
||||
|
||||
// Begin fetching the request
|
||||
//
|
||||
// The delegate can optionally implement the finished selectors or pass NULL
|
||||
// for it.
|
||||
//
|
||||
// Returns YES if the fetch is initiated. The delegate is retained between
|
||||
// the beginFetch call until after the finish callback.
|
||||
//
|
||||
// An error is passed to the callback for server statuses 300 or
|
||||
// higher, with the status stored as the error object's code.
|
||||
//
|
||||
// finishedSEL has a signature like:
|
||||
// - (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error;
|
||||
//
|
||||
// If the application has specified a downloadPath or downloadFileHandle
|
||||
// for the fetcher, the data parameter passed to the callback will be nil.
|
||||
|
||||
- (BOOL)beginFetchWithDelegate:(id)delegate
|
||||
didFinishSelector:(SEL)finishedSEL;
|
||||
|
||||
#if NS_BLOCKS_AVAILABLE
|
||||
- (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler;
|
||||
#endif
|
||||
|
||||
|
||||
// Returns YES if this is in the process of fetching a URL
|
||||
- (BOOL)isFetching;
|
||||
|
||||
// Cancel the fetch of the request that's currently in progress
|
||||
- (void)stopFetching;
|
||||
|
||||
// Return the status code from the server response
|
||||
@property (readonly) NSInteger statusCode;
|
||||
|
||||
// Return the http headers from the response
|
||||
@property (retain, readonly) NSDictionary *responseHeaders;
|
||||
|
||||
// The response, once it's been received
|
||||
@property (retain) NSURLResponse *response;
|
||||
|
||||
// Bytes downloaded so far
|
||||
@property (readonly) unsigned long long downloadedLength;
|
||||
|
||||
// Buffer of currently-downloaded data
|
||||
@property (readonly, retain) NSData *downloadedData;
|
||||
|
||||
// Path in which to non-atomically create a file for storing the downloaded data
|
||||
//
|
||||
// The path must be set before fetching begins. The download file handle
|
||||
// will be created for the path, and can be used to monitor progress. If a file
|
||||
// already exists at the path, it will be overwritten.
|
||||
@property (copy) NSString *downloadPath;
|
||||
|
||||
// If downloadFileHandle is set, data received is immediately appended to
|
||||
// the file handle rather than being accumulated in the downloadedData property
|
||||
//
|
||||
// The file handle supplied must allow writing and support seekToFileOffset:,
|
||||
// and must be set before fetching begins. Setting a download path will
|
||||
// override the file handle property.
|
||||
@property (retain) NSFileHandle *downloadFileHandle;
|
||||
|
||||
// The optional fetchHistory object is used for a sequence of fetchers to
|
||||
// remember ETags, cache ETagged data, and store cookies. Typically, this
|
||||
// is set by a GTMFetcherService object when it creates a fetcher.
|
||||
//
|
||||
// Side effect: setting fetch history implicitly calls setCookieStorageMethod:
|
||||
@property (retain) id <GTMHTTPFetchHistoryProtocol> fetchHistory;
|
||||
|
||||
// userData is retained for the convenience of the caller
|
||||
@property (retain) id userData;
|
||||
|
||||
// Stored property values are retained for the convenience of the caller
|
||||
@property (copy) NSMutableDictionary *properties;
|
||||
|
||||
- (void)setProperty:(id)obj forKey:(NSString *)key; // pass nil obj to remove property
|
||||
- (id)propertyForKey:(NSString *)key;
|
||||
|
||||
- (void)addPropertiesFromDictionary:(NSDictionary *)dict;
|
||||
|
||||
// Comments are useful for logging
|
||||
@property (copy) NSString *comment;
|
||||
|
||||
- (void)setCommentWithFormat:(id)format, ...;
|
||||
|
||||
// Log of request and response, if logging is enabled
|
||||
@property (copy) NSString *log;
|
||||
|
||||
// Using the fetcher while a modal dialog is displayed requires setting the
|
||||
// run-loop modes to include NSModalPanelRunLoopMode
|
||||
@property (retain) NSArray *runLoopModes;
|
||||
|
||||
// Users who wish to replace GTMHTTPFetcher's use of NSURLConnection
|
||||
// can do so globally here. The replacement should be a subclass of
|
||||
// NSURLConnection.
|
||||
+ (Class)connectionClass;
|
||||
+ (void)setConnectionClass:(Class)theClass;
|
||||
|
||||
// Spin the run loop, discarding events, until the fetch has completed
|
||||
//
|
||||
// This is only for use in testing or in tools without a user interface.
|
||||
//
|
||||
// Synchronous fetches should never be done by shipping apps; they are
|
||||
// sufficient reason for rejection from the app store.
|
||||
- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds;
|
||||
|
||||
#if STRIP_GTM_FETCH_LOGGING
|
||||
// if logging is stripped, provide a stub for the main method
|
||||
// for controlling logging
|
||||
+ (void)setLoggingEnabled:(BOOL)flag;
|
||||
#endif // STRIP_GTM_FETCH_LOGGING
|
||||
|
||||
@end
|
||||
1746
OAuth 2/GTMHTTPFetcher.m
Executable file
1746
OAuth 2/GTMHTTPFetcher.m
Executable file
File diff suppressed because it is too large
Load Diff
325
OAuth 2/GTMOAuth2Authentication.h
Executable file
325
OAuth 2/GTMOAuth2Authentication.h
Executable file
@@ -0,0 +1,325 @@
|
||||
/* Copyright (c) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|
||||
|
||||
// This class implements the OAuth 2 protocol for authorizing requests.
|
||||
// http://tools.ietf.org/html/draft-ietf-oauth-v2
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// GTMHTTPFetcher.h brings in GTLDefines/GDataDefines
|
||||
#import "GTMHTTPFetcher.h"
|
||||
|
||||
#undef _EXTERN
|
||||
#undef _INITIALIZE_AS
|
||||
#ifdef GTMOAUTH2AUTHENTICATION_DEFINE_GLOBALS
|
||||
#define _EXTERN
|
||||
#define _INITIALIZE_AS(x) =x
|
||||
#else
|
||||
#if defined(__cplusplus)
|
||||
#define _EXTERN extern "C"
|
||||
#else
|
||||
#define _EXTERN extern
|
||||
#endif
|
||||
#define _INITIALIZE_AS(x)
|
||||
#endif
|
||||
|
||||
// Until all OAuth 2 providers are up to the same spec, we'll provide a crude
|
||||
// way here to override the "Bearer" string in the Authorization header
|
||||
#ifndef GTM_OAUTH2_BEARER
|
||||
#define GTM_OAUTH2_BEARER "Bearer"
|
||||
#endif
|
||||
|
||||
// Service provider name allows stored authorization to be associated with
|
||||
// the authorizing service
|
||||
_EXTERN NSString* const kGTMOAuth2ServiceProviderGoogle _INITIALIZE_AS(@"Google");
|
||||
|
||||
//
|
||||
// GTMOAuth2SignIn constants, included here for use by clients
|
||||
//
|
||||
_EXTERN NSString* const kGTMOAuth2ErrorDomain _INITIALIZE_AS(@"com.google.GTMOAuth2");
|
||||
|
||||
// Error userInfo keys
|
||||
_EXTERN NSString* const kGTMOAuth2ErrorMessageKey _INITIALIZE_AS(@"error");
|
||||
_EXTERN NSString* const kGTMOAuth2ErrorRequestKey _INITIALIZE_AS(@"request");
|
||||
_EXTERN NSString* const kGTMOAuth2ErrorJSONKey _INITIALIZE_AS(@"json");
|
||||
|
||||
enum {
|
||||
// Error code indicating that the window was prematurely closed
|
||||
kGTMOAuth2ErrorWindowClosed = -1000,
|
||||
kGTMOAuth2ErrorAuthorizationFailed = -1001,
|
||||
kGTMOAuth2ErrorTokenExpired = -1002,
|
||||
kGTMOAuth2ErrorTokenUnavailable = -1003,
|
||||
kGTMOAuth2ErrorUnauthorizableRequest = -1004
|
||||
};
|
||||
|
||||
|
||||
// Notifications for token fetches
|
||||
_EXTERN NSString* const kGTMOAuth2FetchStarted _INITIALIZE_AS(@"kGTMOAuth2FetchStarted");
|
||||
_EXTERN NSString* const kGTMOAuth2FetchStopped _INITIALIZE_AS(@"kGTMOAuth2FetchStopped");
|
||||
|
||||
_EXTERN NSString* const kGTMOAuth2FetcherKey _INITIALIZE_AS(@"fetcher");
|
||||
_EXTERN NSString* const kGTMOAuth2FetchTypeKey _INITIALIZE_AS(@"FetchType");
|
||||
_EXTERN NSString* const kGTMOAuth2FetchTypeToken _INITIALIZE_AS(@"token");
|
||||
_EXTERN NSString* const kGTMOAuth2FetchTypeRefresh _INITIALIZE_AS(@"refresh");
|
||||
_EXTERN NSString* const kGTMOAuth2FetchTypeAssertion _INITIALIZE_AS(@"assertion");
|
||||
_EXTERN NSString* const kGTMOAuth2FetchTypeUserInfo _INITIALIZE_AS(@"userInfo");
|
||||
|
||||
// Token-issuance errors
|
||||
_EXTERN NSString* const kGTMOAuth2ErrorKey _INITIALIZE_AS(@"error");
|
||||
|
||||
_EXTERN NSString* const kGTMOAuth2ErrorInvalidRequest _INITIALIZE_AS(@"invalid_request");
|
||||
_EXTERN NSString* const kGTMOAuth2ErrorInvalidClient _INITIALIZE_AS(@"invalid_client");
|
||||
_EXTERN NSString* const kGTMOAuth2ErrorInvalidGrant _INITIALIZE_AS(@"invalid_grant");
|
||||
_EXTERN NSString* const kGTMOAuth2ErrorUnauthorizedClient _INITIALIZE_AS(@"unauthorized_client");
|
||||
_EXTERN NSString* const kGTMOAuth2ErrorUnsupportedGrantType _INITIALIZE_AS(@"unsupported_grant_type");
|
||||
_EXTERN NSString* const kGTMOAuth2ErrorInvalidScope _INITIALIZE_AS(@"invalid_scope");
|
||||
|
||||
// Notification that sign-in has completed, and token fetches will begin (useful
|
||||
// for displaying interstitial messages after the window has closed)
|
||||
_EXTERN NSString* const kGTMOAuth2UserSignedIn _INITIALIZE_AS(@"kGTMOAuth2UserSignedIn");
|
||||
|
||||
// Notification for token changes
|
||||
_EXTERN NSString* const kGTMOAuth2AccessTokenRefreshed _INITIALIZE_AS(@"kGTMOAuth2AccessTokenRefreshed");
|
||||
_EXTERN NSString* const kGTMOAuth2RefreshTokenChanged _INITIALIZE_AS(@"kGTMOAuth2RefreshTokenChanged");
|
||||
|
||||
// Notification for WebView loading
|
||||
_EXTERN NSString* const kGTMOAuth2WebViewStartedLoading _INITIALIZE_AS(@"kGTMOAuth2WebViewStartedLoading");
|
||||
_EXTERN NSString* const kGTMOAuth2WebViewStoppedLoading _INITIALIZE_AS(@"kGTMOAuth2WebViewStoppedLoading");
|
||||
_EXTERN NSString* const kGTMOAuth2WebViewKey _INITIALIZE_AS(@"kGTMOAuth2WebViewKey");
|
||||
_EXTERN NSString* const kGTMOAuth2WebViewStopKindKey _INITIALIZE_AS(@"kGTMOAuth2WebViewStopKindKey");
|
||||
_EXTERN NSString* const kGTMOAuth2WebViewFinished _INITIALIZE_AS(@"finished");
|
||||
_EXTERN NSString* const kGTMOAuth2WebViewFailed _INITIALIZE_AS(@"failed");
|
||||
_EXTERN NSString* const kGTMOAuth2WebViewCancelled _INITIALIZE_AS(@"cancelled");
|
||||
|
||||
// Notification for network loss during html sign-in display
|
||||
_EXTERN NSString* const kGTMOAuth2NetworkLost _INITIALIZE_AS(@"kGTMOAuthNetworkLost");
|
||||
_EXTERN NSString* const kGTMOAuth2NetworkFound _INITIALIZE_AS(@"kGTMOAuthNetworkFound");
|
||||
|
||||
@interface GTMOAuth2Authentication : NSObject <GTMFetcherAuthorizationProtocol> {
|
||||
@private
|
||||
NSString *clientID_;
|
||||
NSString *clientSecret_;
|
||||
NSString *redirectURI_;
|
||||
NSMutableDictionary *parameters_;
|
||||
|
||||
// authorization parameters
|
||||
NSURL *tokenURL_;
|
||||
NSDate *expirationDate_;
|
||||
|
||||
NSDictionary *additionalTokenRequestParameters_;
|
||||
|
||||
// queue of requests for authorization waiting for a valid access token
|
||||
GTMHTTPFetcher *refreshFetcher_;
|
||||
NSMutableArray *authorizationQueue_;
|
||||
|
||||
id <GTMHTTPFetcherServiceProtocol> fetcherService_; // WEAK
|
||||
|
||||
Class parserClass_;
|
||||
|
||||
BOOL shouldAuthorizeAllRequests_;
|
||||
|
||||
// arbitrary data retained for the user
|
||||
id userData_;
|
||||
NSMutableDictionary *properties_;
|
||||
}
|
||||
|
||||
// OAuth2 standard protocol parameters
|
||||
//
|
||||
// These should be the plain strings; any needed escaping will be provided by
|
||||
// the library.
|
||||
|
||||
// Request properties
|
||||
@property (copy) NSString *clientID;
|
||||
@property (copy) NSString *clientSecret;
|
||||
@property (copy) NSString *redirectURI;
|
||||
@property (retain) NSString *scope;
|
||||
@property (retain) NSString *tokenType;
|
||||
@property (retain) NSString *assertion;
|
||||
|
||||
// Apps may optionally add parameters here to be provided to the token
|
||||
// endpoint on token requests and refreshes
|
||||
@property (retain) NSDictionary *additionalTokenRequestParameters;
|
||||
|
||||
// Response properties
|
||||
@property (retain) NSMutableDictionary *parameters;
|
||||
|
||||
@property (retain) NSString *accessToken;
|
||||
@property (retain) NSString *refreshToken;
|
||||
@property (retain) NSNumber *expiresIn;
|
||||
@property (retain) NSString *code;
|
||||
@property (retain) NSString *errorString;
|
||||
|
||||
// URL for obtaining access tokens
|
||||
@property (copy) NSURL *tokenURL;
|
||||
|
||||
// Calculated expiration date (expiresIn seconds added to the
|
||||
// time the access token was received.)
|
||||
@property (copy) NSDate *expirationDate;
|
||||
|
||||
// Service identifier, like "Google"; not used for authentication
|
||||
//
|
||||
// The provider name is just for allowing stored authorization to be associated
|
||||
// with the authorizing service.
|
||||
@property (copy) NSString *serviceProvider;
|
||||
|
||||
// User email and verified status; not used for authentication
|
||||
//
|
||||
// The verified string can be checked with -boolValue. If the result is false,
|
||||
// then the email address is listed with the account on the server, but the
|
||||
// address has not been confirmed as belonging to the owner of the account.
|
||||
@property (retain) NSString *userEmail;
|
||||
@property (retain) NSString *userEmailIsVerified;
|
||||
|
||||
// Property indicating if this auth has a refresh token so is suitable for
|
||||
// authorizing a request. This does not guarantee that the token is valid.
|
||||
@property (readonly) BOOL canAuthorize;
|
||||
|
||||
// Property indicating if this object will authorize plain http request
|
||||
// (as well as any non-https requests.) Default is NO, only requests with the
|
||||
// scheme https are authorized, since security may be compromised if tokens
|
||||
// are sent over the wire using an unencrypted protocol like http.
|
||||
@property (assign) BOOL shouldAuthorizeAllRequests;
|
||||
|
||||
// userData is retained for the convenience of the caller
|
||||
@property (retain) id userData;
|
||||
|
||||
// Stored property values are retained for the convenience of the caller
|
||||
@property (retain) NSDictionary *properties;
|
||||
|
||||
// Property for the optional fetcher service instance to be used to create
|
||||
// fetchers
|
||||
//
|
||||
// Fetcher service objects retain authorizations, so this is weak to avoid
|
||||
// circular retains.
|
||||
@property (assign) id <GTMHTTPFetcherServiceProtocol> fetcherService; // WEAK
|
||||
|
||||
// Alternative JSON parsing class; this should implement the
|
||||
// GTMOAuth2ParserClass informal protocol. If this property is
|
||||
// not set, the class SBJSON must be available in the runtime.
|
||||
@property (assign) Class parserClass;
|
||||
|
||||
// Convenience method for creating an authentication object
|
||||
+ (id)authenticationWithServiceProvider:(NSString *)serviceProvider
|
||||
tokenURL:(NSURL *)tokenURL
|
||||
redirectURI:(NSString *)redirectURI
|
||||
clientID:(NSString *)clientID
|
||||
clientSecret:(NSString *)clientSecret;
|
||||
|
||||
// Clear out any authentication values, prepare for a new request fetch
|
||||
- (void)reset;
|
||||
|
||||
// Main authorization entry points
|
||||
//
|
||||
// These will refresh the access token, if necessary, add the access token to
|
||||
// the request, then invoke the callback.
|
||||
//
|
||||
// The request argument may be nil to just force a refresh of the access token,
|
||||
// if needed.
|
||||
//
|
||||
// NOTE: To avoid accidental leaks of bearer tokens, the request must
|
||||
// be for a URL with the scheme https unless the shouldAuthorizeAllRequests
|
||||
// property is set.
|
||||
|
||||
// The finish selector should have a signature matching
|
||||
// - (void)authentication:(GTMOAuth2Authentication *)auth
|
||||
// request:(NSMutableURLRequest *)request
|
||||
// finishedWithError:(NSError *)error;
|
||||
|
||||
- (void)authorizeRequest:(NSMutableURLRequest *)request
|
||||
delegate:(id)delegate
|
||||
didFinishSelector:(SEL)sel;
|
||||
|
||||
#if NS_BLOCKS_AVAILABLE
|
||||
- (void)authorizeRequest:(NSMutableURLRequest *)request
|
||||
completionHandler:(void (^)(NSError *error))handler;
|
||||
#endif
|
||||
|
||||
// Synchronous entry point; authorizing this way cannot refresh an expired
|
||||
// access token
|
||||
- (BOOL)authorizeRequest:(NSMutableURLRequest *)request;
|
||||
|
||||
// If the authentication is waiting for a refresh to complete, spin the run
|
||||
// loop, discarding events, until the fetch has completed
|
||||
//
|
||||
// This is only for use in testing or in tools without a user interface.
|
||||
- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds;
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Internal properties and methods for use by GTMOAuth2SignIn
|
||||
//
|
||||
|
||||
// Pending fetcher to get a new access token, if any
|
||||
@property (retain) GTMHTTPFetcher *refreshFetcher;
|
||||
|
||||
// Check if a request is queued up to be authorized
|
||||
- (BOOL)isAuthorizingRequest:(NSURLRequest *)request;
|
||||
|
||||
// Check if a request appears to be authorized
|
||||
- (BOOL)isAuthorizedRequest:(NSURLRequest *)request;
|
||||
|
||||
// Stop any pending refresh fetch
|
||||
- (void)stopAuthorization;
|
||||
|
||||
// OAuth fetch user-agent header value
|
||||
- (NSString *)userAgent;
|
||||
|
||||
// Parse and set token and token secret from response data
|
||||
- (void)setKeysForResponseString:(NSString *)str;
|
||||
- (void)setKeysForResponseDictionary:(NSDictionary *)dict;
|
||||
|
||||
// Persistent token string for keychain storage
|
||||
//
|
||||
// We'll use the format "refresh_token=foo&serviceProvider=bar" so we can
|
||||
// easily alter what portions of the auth data are stored
|
||||
//
|
||||
// Use these methods for serialization
|
||||
- (NSString *)persistenceResponseString;
|
||||
- (void)setKeysForPersistenceResponseString:(NSString *)str;
|
||||
|
||||
// method to begin fetching an access token, used by the sign-in object
|
||||
- (GTMHTTPFetcher *)beginTokenFetchWithDelegate:(id)delegate
|
||||
didFinishSelector:(SEL)finishedSel;
|
||||
|
||||
// Entry point to post a notification about a fetcher currently used for
|
||||
// obtaining or refreshing a token; the sign-in object will also use this
|
||||
// to indicate when the user's email address is being fetched.
|
||||
//
|
||||
// Fetch type constants are above under "notifications for token fetches"
|
||||
- (void)notifyFetchIsRunning:(BOOL)isStarting
|
||||
fetcher:(GTMHTTPFetcher *)fetcher
|
||||
type:(NSString *)fetchType;
|
||||
|
||||
// Arbitrary key-value properties retained for the user
|
||||
- (void)setProperty:(id)obj forKey:(NSString *)key;
|
||||
- (id)propertyForKey:(NSString *)key;
|
||||
|
||||
//
|
||||
// Utilities
|
||||
//
|
||||
|
||||
+ (NSString *)encodedOAuthValueForString:(NSString *)str;
|
||||
|
||||
+ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict;
|
||||
|
||||
+ (NSDictionary *)dictionaryWithResponseString:(NSString *)responseStr;
|
||||
|
||||
+ (NSString *)scopeWithStrings:(NSString *)firsStr, ... NS_REQUIRES_NIL_TERMINATION;
|
||||
@end
|
||||
|
||||
#endif // GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|
||||
1166
OAuth 2/GTMOAuth2Authentication.m
Executable file
1166
OAuth 2/GTMOAuth2Authentication.m
Executable file
File diff suppressed because it is too large
Load Diff
184
OAuth 2/GTMOAuth2SignIn.h
Executable file
184
OAuth 2/GTMOAuth2SignIn.h
Executable file
@@ -0,0 +1,184 @@
|
||||
/* Copyright (c) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//
|
||||
// This sign-in object opens and closes the web view window as needed for
|
||||
// users to sign in. For signing in to Google, it also obtains
|
||||
// the authenticated user's email address.
|
||||
//
|
||||
// Typically, this will be managed for the application by
|
||||
// GTMOAuth2ViewControllerTouch or GTMOAuth2WindowController, so this
|
||||
// class's interface is interesting only if
|
||||
// you are creating your own window controller for sign-in.
|
||||
//
|
||||
//
|
||||
// Delegate methods implemented by the window controller
|
||||
//
|
||||
// The window controller implements two methods for use by the sign-in object,
|
||||
// the webRequestSelector and the finishedSelector:
|
||||
//
|
||||
// webRequestSelector has a signature matching
|
||||
// - (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request
|
||||
//
|
||||
// The web request selector will be invoked with a request to be displayed, or
|
||||
// nil to close the window when the final callback request has been encountered.
|
||||
//
|
||||
//
|
||||
// finishedSelector has a signature matching
|
||||
// - (void)signin:(GTMOAuth2SignIn *)signin finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error
|
||||
//
|
||||
// The finished selector will be invoked when sign-in has completed, except
|
||||
// when explicitly canceled by calling cancelSigningIn
|
||||
//
|
||||
|
||||
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
|
||||
// GTMHTTPFetcher brings in GTLDefines/GDataDefines
|
||||
#import "GTMHTTPFetcher.h"
|
||||
|
||||
#import "GTMOAuth2Authentication.h"
|
||||
|
||||
@class GTMOAuth2SignIn;
|
||||
|
||||
@interface GTMOAuth2SignIn : NSObject {
|
||||
@private
|
||||
GTMOAuth2Authentication *auth_;
|
||||
|
||||
// the endpoint for displaying the sign-in page
|
||||
NSURL *authorizationURL_;
|
||||
NSDictionary *additionalAuthorizationParameters_;
|
||||
|
||||
id delegate_;
|
||||
SEL webRequestSelector_;
|
||||
SEL finishedSelector_;
|
||||
|
||||
BOOL hasHandledCallback_;
|
||||
|
||||
GTMHTTPFetcher *pendingFetcher_;
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
BOOL shouldFetchGoogleUserEmail_;
|
||||
BOOL shouldFetchGoogleUserProfile_;
|
||||
NSDictionary *userProfile_;
|
||||
#endif
|
||||
|
||||
SCNetworkReachabilityRef reachabilityRef_;
|
||||
NSTimer *networkLossTimer_;
|
||||
NSTimeInterval networkLossTimeoutInterval_;
|
||||
BOOL hasNotifiedNetworkLoss_;
|
||||
|
||||
id userData_;
|
||||
}
|
||||
|
||||
@property (nonatomic, retain) GTMOAuth2Authentication *authentication;
|
||||
|
||||
@property (nonatomic, retain) NSURL *authorizationURL;
|
||||
@property (nonatomic, retain) NSDictionary *additionalAuthorizationParameters;
|
||||
|
||||
// The delegate is released when signing in finishes or is cancelled
|
||||
@property (nonatomic, retain) id delegate;
|
||||
@property (nonatomic, assign) SEL webRequestSelector;
|
||||
@property (nonatomic, assign) SEL finishedSelector;
|
||||
|
||||
@property (nonatomic, retain) id userData;
|
||||
|
||||
// By default, signing in to Google will fetch the user's email, but will not
|
||||
// fetch the user's profile.
|
||||
//
|
||||
// The email is saved in the auth object.
|
||||
// The profile is available immediately after sign-in.
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
@property (nonatomic, assign) BOOL shouldFetchGoogleUserEmail;
|
||||
@property (nonatomic, assign) BOOL shouldFetchGoogleUserProfile;
|
||||
@property (nonatomic, retain, readonly) NSDictionary *userProfile;
|
||||
#endif
|
||||
|
||||
// The default timeout for an unreachable network during display of the
|
||||
// sign-in page is 30 seconds; set this to 0 to have no timeout
|
||||
@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval;
|
||||
|
||||
// The delegate is retained until sign-in has completed or been canceled
|
||||
//
|
||||
// designated initializer
|
||||
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
|
||||
authorizationURL:(NSURL *)authorizationURL
|
||||
delegate:(id)delegate
|
||||
webRequestSelector:(SEL)webRequestSelector
|
||||
finishedSelector:(SEL)finishedSelector;
|
||||
|
||||
// A default authentication object for signing in to Google services
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
+ (GTMOAuth2Authentication *)standardGoogleAuthenticationForScope:(NSString *)scope
|
||||
clientID:(NSString *)clientID
|
||||
clientSecret:(NSString *)clientSecret;
|
||||
#endif
|
||||
|
||||
#pragma mark Methods used by the Window Controller
|
||||
|
||||
// Start the sequence of fetches and sign-in window display for sign-in
|
||||
- (BOOL)startSigningIn;
|
||||
|
||||
// Stop any pending fetches, and close the window (but don't call the
|
||||
// delegate's finishedSelector)
|
||||
- (void)cancelSigningIn;
|
||||
|
||||
// Window controllers must tell the sign-in object about any redirect
|
||||
// requested by the web view, and any changes in the webview window title
|
||||
//
|
||||
// If these return YES then the event was handled by the
|
||||
// sign-in object (typically by closing the window) and should be ignored by
|
||||
// the window controller's web view
|
||||
|
||||
- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest;
|
||||
- (BOOL)titleChanged:(NSString *)title;
|
||||
- (BOOL)cookiesChanged:(NSHTTPCookieStorage *)cookieStorage;
|
||||
- (BOOL)loadFailedWithError:(NSError *)error;
|
||||
|
||||
// Window controllers must tell the sign-in object if the window was closed
|
||||
// prematurely by the user (but not by the sign-in object); this calls the
|
||||
// delegate's finishedSelector
|
||||
- (void)windowWasClosed;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
// Revocation of an authorized token from Google
|
||||
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth;
|
||||
|
||||
// Create a fetcher for obtaining the user's Google email address or profile,
|
||||
// according to the current auth scopes.
|
||||
//
|
||||
// The auth object must have been created with appropriate scopes.
|
||||
//
|
||||
// The fetcher's response data can be parsed with NSJSONSerialization.
|
||||
+ (GTMHTTPFetcher *)userInfoFetcherWithAuth:(GTMOAuth2Authentication *)auth;
|
||||
#endif
|
||||
|
||||
#pragma mark -
|
||||
|
||||
// Standard authentication values
|
||||
+ (NSString *)nativeClientRedirectURI;
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
+ (NSURL *)googleAuthorizationURL;
|
||||
+ (NSURL *)googleTokenURL;
|
||||
+ (NSURL *)googleUserInfoURL;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|
||||
814
OAuth 2/GTMOAuth2SignIn.m
Executable file
814
OAuth 2/GTMOAuth2SignIn.m
Executable file
@@ -0,0 +1,814 @@
|
||||
/* Copyright (c) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|
||||
|
||||
#define GTMOAUTH2SIGNIN_DEFINE_GLOBALS 1
|
||||
#import "GTMOAuth2SignIn.h"
|
||||
|
||||
// we'll default to timing out if the network becomes unreachable for more
|
||||
// than 30 seconds when the sign-in page is displayed
|
||||
static const NSTimeInterval kDefaultNetworkLossTimeoutInterval = 30.0;
|
||||
|
||||
// URI indicating an installed app is signing in. This is described at
|
||||
//
|
||||
// http://code.google.com/apis/accounts/docs/OAuth2.html#IA
|
||||
//
|
||||
NSString *const kOOBString = @"urn:ietf:wg:oauth:2.0:oob";
|
||||
|
||||
|
||||
@interface GTMOAuth2Authentication (InternalMethods)
|
||||
- (NSDictionary *)dictionaryWithJSONData:(NSData *)data;
|
||||
@end
|
||||
|
||||
@interface GTMOAuth2SignIn ()
|
||||
@property (assign) BOOL hasHandledCallback;
|
||||
@property (retain) GTMHTTPFetcher *pendingFetcher;
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
@property (nonatomic, retain, readwrite) NSDictionary *userProfile;
|
||||
#endif
|
||||
|
||||
- (void)invokeFinalCallbackWithError:(NSError *)error;
|
||||
|
||||
- (BOOL)startWebRequest;
|
||||
+ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL
|
||||
paramString:(NSString *)paramStr;
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
- (void)fetchGoogleUserInfo;
|
||||
#endif
|
||||
- (void)finishSignInWithError:(NSError *)error;
|
||||
|
||||
- (void)handleCallbackReached;
|
||||
|
||||
- (void)auth:(GTMOAuth2Authentication *)auth
|
||||
finishedWithFetcher:(GTMHTTPFetcher *)fetcher
|
||||
error:(NSError *)error;
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
- (void)infoFetcher:(GTMHTTPFetcher *)fetcher
|
||||
finishedWithData:(NSData *)data
|
||||
error:(NSError *)error;
|
||||
#endif
|
||||
|
||||
- (void)closeTheWindow;
|
||||
|
||||
- (void)startReachabilityCheck;
|
||||
- (void)stopReachabilityCheck;
|
||||
- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef
|
||||
changedFlags:(SCNetworkConnectionFlags)flags;
|
||||
- (void)reachabilityTimerFired:(NSTimer *)timer;
|
||||
@end
|
||||
|
||||
@implementation GTMOAuth2SignIn
|
||||
|
||||
@synthesize authentication = auth_;
|
||||
|
||||
@synthesize authorizationURL = authorizationURL_;
|
||||
@synthesize additionalAuthorizationParameters = additionalAuthorizationParameters_;
|
||||
|
||||
@synthesize delegate = delegate_;
|
||||
@synthesize webRequestSelector = webRequestSelector_;
|
||||
@synthesize finishedSelector = finishedSelector_;
|
||||
@synthesize hasHandledCallback = hasHandledCallback_;
|
||||
@synthesize pendingFetcher = pendingFetcher_;
|
||||
@synthesize userData = userData_;
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
@synthesize shouldFetchGoogleUserEmail = shouldFetchGoogleUserEmail_;
|
||||
@synthesize shouldFetchGoogleUserProfile = shouldFetchGoogleUserProfile_;
|
||||
@synthesize userProfile = userProfile_;
|
||||
#endif
|
||||
|
||||
@synthesize networkLossTimeoutInterval = networkLossTimeoutInterval_;
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
+ (NSURL *)googleAuthorizationURL {
|
||||
NSString *str = @"https://accounts.google.com/o/oauth2/auth";
|
||||
return [NSURL URLWithString:str];
|
||||
}
|
||||
|
||||
+ (NSURL *)googleTokenURL {
|
||||
NSString *str = @"https://accounts.google.com/o/oauth2/token";
|
||||
return [NSURL URLWithString:str];
|
||||
}
|
||||
|
||||
+ (NSURL *)googleRevocationURL {
|
||||
NSString *urlStr = @"https://accounts.google.com/o/oauth2/revoke";
|
||||
return [NSURL URLWithString:urlStr];
|
||||
}
|
||||
|
||||
+ (NSURL *)googleUserInfoURL {
|
||||
NSString *urlStr = @"https://www.googleapis.com/oauth2/v1/userinfo";
|
||||
return [NSURL URLWithString:urlStr];
|
||||
}
|
||||
#endif
|
||||
|
||||
+ (NSString *)nativeClientRedirectURI {
|
||||
return kOOBString;
|
||||
}
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
+ (GTMOAuth2Authentication *)standardGoogleAuthenticationForScope:(NSString *)scope
|
||||
clientID:(NSString *)clientID
|
||||
clientSecret:(NSString *)clientSecret {
|
||||
NSString *redirectURI = [self nativeClientRedirectURI];
|
||||
NSURL *tokenURL = [self googleTokenURL];
|
||||
|
||||
GTMOAuth2Authentication *auth;
|
||||
auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
|
||||
tokenURL:tokenURL
|
||||
redirectURI:redirectURI
|
||||
clientID:clientID
|
||||
clientSecret:clientSecret];
|
||||
auth.scope = scope;
|
||||
|
||||
return auth;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
|
||||
authorizationURL:(NSURL *)authorizationURL
|
||||
delegate:(id)delegate
|
||||
webRequestSelector:(SEL)webRequestSelector
|
||||
finishedSelector:(SEL)finishedSelector {
|
||||
// check the selectors on debug builds
|
||||
GTMAssertSelectorNilOrImplementedWithArgs(delegate, webRequestSelector,
|
||||
@encode(GTMOAuth2SignIn *), @encode(NSURLRequest *), 0);
|
||||
GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector,
|
||||
@encode(GTMOAuth2SignIn *), @encode(GTMOAuth2Authentication *),
|
||||
@encode(NSError *), 0);
|
||||
|
||||
// designated initializer
|
||||
self = [super init];
|
||||
if (self) {
|
||||
auth_ = [auth retain];
|
||||
authorizationURL_ = [authorizationURL retain];
|
||||
delegate_ = [delegate retain];
|
||||
webRequestSelector_ = webRequestSelector;
|
||||
finishedSelector_ = finishedSelector;
|
||||
|
||||
// for Google authentication, we want to automatically fetch user info
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
NSString *host = [authorizationURL host];
|
||||
if ([host hasSuffix:@".google.com"]) {
|
||||
shouldFetchGoogleUserEmail_ = YES;
|
||||
}
|
||||
#endif
|
||||
|
||||
// default timeout for a lost internet connection while the server
|
||||
// UI is displayed is 30 seconds
|
||||
networkLossTimeoutInterval_ = kDefaultNetworkLossTimeoutInterval;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stopReachabilityCheck];
|
||||
|
||||
[auth_ release];
|
||||
[authorizationURL_ release];
|
||||
[additionalAuthorizationParameters_ release];
|
||||
[delegate_ release];
|
||||
[pendingFetcher_ release];
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
[userProfile_ release];
|
||||
#endif
|
||||
[userData_ release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
#pragma mark Sign-in Sequence Methods
|
||||
|
||||
// stop any pending fetches, and close the window (but don't call the
|
||||
// delegate's finishedSelector)
|
||||
- (void)cancelSigningIn {
|
||||
[self.pendingFetcher stopFetching];
|
||||
self.pendingFetcher = nil;
|
||||
|
||||
[self.authentication stopAuthorization];
|
||||
|
||||
[self closeTheWindow];
|
||||
|
||||
[delegate_ autorelease];
|
||||
delegate_ = nil;
|
||||
}
|
||||
|
||||
//
|
||||
// This is the entry point to begin the sequence
|
||||
// - display the authentication web page, and monitor redirects
|
||||
// - exchange the code for an access token and a refresh token
|
||||
// - for Google sign-in, fetch the user's email address
|
||||
// - tell the delegate we're finished
|
||||
//
|
||||
- (BOOL)startSigningIn {
|
||||
// For signing in to Google, append the scope for obtaining the authenticated
|
||||
// user email and profile, as appropriate
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
GTMOAuth2Authentication *auth = self.authentication;
|
||||
if (self.shouldFetchGoogleUserEmail) {
|
||||
NSString *const emailScope = @"https://www.googleapis.com/auth/userinfo.email";
|
||||
NSString *scope = auth.scope;
|
||||
if ([scope rangeOfString:emailScope].location == NSNotFound) {
|
||||
scope = [GTMOAuth2Authentication scopeWithStrings:scope, emailScope, nil];
|
||||
auth.scope = scope;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.shouldFetchGoogleUserProfile) {
|
||||
NSString *const profileScope = @"https://www.googleapis.com/auth/userinfo.profile";
|
||||
NSString *scope = auth.scope;
|
||||
if ([scope rangeOfString:profileScope].location == NSNotFound) {
|
||||
scope = [GTMOAuth2Authentication scopeWithStrings:scope, profileScope, nil];
|
||||
auth.scope = scope;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// start the authorization
|
||||
return [self startWebRequest];
|
||||
}
|
||||
|
||||
- (NSMutableDictionary *)parametersForWebRequest {
|
||||
GTMOAuth2Authentication *auth = self.authentication;
|
||||
NSString *clientID = auth.clientID;
|
||||
NSString *redirectURI = auth.redirectURI;
|
||||
|
||||
BOOL hasClientID = ([clientID length] > 0);
|
||||
BOOL hasRedirect = ([redirectURI length] > 0
|
||||
|| redirectURI == [[self class] nativeClientRedirectURI]);
|
||||
if (!hasClientID || !hasRedirect) {
|
||||
#if DEBUG
|
||||
NSAssert(hasClientID, @"GTMOAuth2SignIn: clientID needed");
|
||||
NSAssert(hasRedirect, @"GTMOAuth2SignIn: redirectURI needed");
|
||||
#endif
|
||||
return NO;
|
||||
}
|
||||
|
||||
// invoke the UI controller's web request selector to display
|
||||
// the authorization page
|
||||
|
||||
// add params to the authorization URL
|
||||
NSString *scope = auth.scope;
|
||||
if ([scope length] == 0) scope = nil;
|
||||
|
||||
NSMutableDictionary *paramsDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
|
||||
@"code", @"response_type",
|
||||
clientID, @"client_id",
|
||||
scope, @"scope", // scope may be nil
|
||||
nil];
|
||||
if (redirectURI) {
|
||||
[paramsDict setObject:redirectURI forKey:@"redirect_uri"];
|
||||
}
|
||||
return paramsDict;
|
||||
}
|
||||
|
||||
- (BOOL)startWebRequest {
|
||||
NSMutableDictionary *paramsDict = [self parametersForWebRequest];
|
||||
|
||||
NSDictionary *additionalParams = self.additionalAuthorizationParameters;
|
||||
if (additionalParams) {
|
||||
[paramsDict addEntriesFromDictionary:additionalParams];
|
||||
}
|
||||
|
||||
NSString *paramStr = [GTMOAuth2Authentication encodedQueryParametersForDictionary:paramsDict];
|
||||
|
||||
NSURL *authorizationURL = self.authorizationURL;
|
||||
NSMutableURLRequest *request;
|
||||
request = [[self class] mutableURLRequestWithURL:authorizationURL
|
||||
paramString:paramStr];
|
||||
|
||||
[delegate_ performSelector:self.webRequestSelector
|
||||
withObject:self
|
||||
withObject:request];
|
||||
|
||||
// at this point, we're waiting on the server-driven html UI, so
|
||||
// we want notification if we lose connectivity to the web server
|
||||
[self startReachabilityCheck];
|
||||
return YES;
|
||||
}
|
||||
|
||||
// utility for making a request from an old URL with some additional parameters
|
||||
+ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL
|
||||
paramString:(NSString *)paramStr {
|
||||
NSString *query = [oldURL query];
|
||||
if ([query length] > 0) {
|
||||
query = [query stringByAppendingFormat:@"&%@", paramStr];
|
||||
} else {
|
||||
query = paramStr;
|
||||
}
|
||||
|
||||
NSString *portStr = @"";
|
||||
NSString *oldPort = [[oldURL port] stringValue];
|
||||
if ([oldPort length] > 0) {
|
||||
portStr = [@":" stringByAppendingString:oldPort];
|
||||
}
|
||||
|
||||
NSString *qMark = [query length] > 0 ? @"?" : @"";
|
||||
NSString *newURLStr = [NSString stringWithFormat:@"%@://%@%@%@%@%@",
|
||||
[oldURL scheme], [oldURL host], portStr,
|
||||
[oldURL path], qMark, query];
|
||||
NSURL *newURL = [NSURL URLWithString:newURLStr];
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:newURL];
|
||||
return request;
|
||||
}
|
||||
|
||||
// entry point for the window controller to tell us that the window
|
||||
// prematurely closed
|
||||
- (void)windowWasClosed {
|
||||
[self stopReachabilityCheck];
|
||||
|
||||
NSError *error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
|
||||
code:kGTMOAuth2ErrorWindowClosed
|
||||
userInfo:nil];
|
||||
[self invokeFinalCallbackWithError:error];
|
||||
}
|
||||
|
||||
// internal method to tell the window controller to close the window
|
||||
- (void)closeTheWindow {
|
||||
[self stopReachabilityCheck];
|
||||
|
||||
// a nil request means the window should be closed
|
||||
[delegate_ performSelector:self.webRequestSelector
|
||||
withObject:self
|
||||
withObject:nil];
|
||||
}
|
||||
|
||||
// entry point for the window controller to tell us what web page has been
|
||||
// requested
|
||||
//
|
||||
// When the request is for the callback URL, this method invokes
|
||||
// handleCallbackReached and returns YES
|
||||
- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest {
|
||||
// for Google's installed app sign-in protocol, we'll look for the
|
||||
// end-of-sign-in indicator in the titleChanged: method below
|
||||
NSString *redirectURI = self.authentication.redirectURI;
|
||||
if (redirectURI == nil) return NO;
|
||||
|
||||
// when we're searching for the window title string, then we can ignore
|
||||
// redirects
|
||||
NSString *standardURI = [[self class] nativeClientRedirectURI];
|
||||
if (standardURI != nil && [redirectURI isEqual:standardURI]) return NO;
|
||||
|
||||
// compare the redirectURI, which tells us when the web sign-in is done,
|
||||
// to the actual redirection
|
||||
NSURL *redirectURL = [NSURL URLWithString:redirectURI];
|
||||
NSURL *requestURL = [redirectedRequest URL];
|
||||
|
||||
// avoid comparing to nil host and path values (such as when redirected to
|
||||
// "about:blank")
|
||||
NSString *requestHost = [requestURL host];
|
||||
NSString *requestPath = [requestURL path];
|
||||
BOOL isCallback;
|
||||
if (requestHost && requestPath) {
|
||||
isCallback = [[redirectURL host] isEqual:[requestURL host]]
|
||||
&& [[redirectURL path] isEqual:[requestURL path]];
|
||||
} else if (requestURL) {
|
||||
// handle "about:blank"
|
||||
isCallback = [redirectURL isEqual:requestURL];
|
||||
} else {
|
||||
isCallback = NO;
|
||||
}
|
||||
|
||||
if (!isCallback) {
|
||||
// tell the caller that this request is nothing interesting
|
||||
return NO;
|
||||
}
|
||||
|
||||
// we've reached the callback URL
|
||||
|
||||
// try to get the access code
|
||||
if (!self.hasHandledCallback) {
|
||||
NSString *responseStr = [[redirectedRequest URL] query];
|
||||
[self.authentication setKeysForResponseString:responseStr];
|
||||
|
||||
#if DEBUG
|
||||
NSAssert([self.authentication.code length] > 0
|
||||
|| [self.authentication.errorString length] > 0,
|
||||
@"response lacks auth code or error");
|
||||
#endif
|
||||
|
||||
[self handleCallbackReached];
|
||||
}
|
||||
// tell the delegate that we did handle this request
|
||||
return YES;
|
||||
}
|
||||
|
||||
// entry point for the window controller to tell us when a new page title has
|
||||
// been loadded
|
||||
//
|
||||
// When the title indicates sign-in has completed, this method invokes
|
||||
// handleCallbackReached and returns YES
|
||||
- (BOOL)titleChanged:(NSString *)title {
|
||||
// return YES if the OAuth flow ending title was detected
|
||||
|
||||
// right now we're just looking for a parameter list following the last space
|
||||
// in the title string, but hopefully we'll eventually get something better
|
||||
// from the server to search for
|
||||
NSRange paramsRange = [title rangeOfString:@" "
|
||||
options:NSBackwardsSearch];
|
||||
NSUInteger spaceIndex = paramsRange.location;
|
||||
if (spaceIndex != NSNotFound) {
|
||||
NSString *responseStr = [title substringFromIndex:(spaceIndex + 1)];
|
||||
|
||||
NSDictionary *dict = [GTMOAuth2Authentication dictionaryWithResponseString:responseStr];
|
||||
|
||||
NSString *code = [dict objectForKey:@"code"];
|
||||
NSString *error = [dict objectForKey:@"error"];
|
||||
if ([code length] > 0 || [error length] > 0) {
|
||||
|
||||
if (!self.hasHandledCallback) {
|
||||
[self.authentication setKeysForResponseDictionary:dict];
|
||||
|
||||
[self handleCallbackReached];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)cookiesChanged:(NSHTTPCookieStorage *)cookieStorage {
|
||||
// We're ignoring these.
|
||||
return NO;
|
||||
};
|
||||
|
||||
// entry point for the window controller to tell us when a load has failed
|
||||
// in the webview
|
||||
//
|
||||
// if the initial authorization URL fails, bail out so the user doesn't
|
||||
// see an empty webview
|
||||
- (BOOL)loadFailedWithError:(NSError *)error {
|
||||
NSURL *authorizationURL = self.authorizationURL;
|
||||
NSURL *failedURL = [[error userInfo] valueForKey:@"NSErrorFailingURLKey"]; // NSURLErrorFailingURLErrorKey defined in 10.6
|
||||
|
||||
BOOL isAuthURL = [[failedURL host] isEqual:[authorizationURL host]]
|
||||
&& [[failedURL path] isEqual:[authorizationURL path]];
|
||||
|
||||
if (isAuthURL) {
|
||||
// We can assume that we have no pending fetchers, since we only
|
||||
// handle failure to load the initial authorization URL
|
||||
[self closeTheWindow];
|
||||
[self invokeFinalCallbackWithError:error];
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)handleCallbackReached {
|
||||
// the callback page was requested, or the authenticate code was loaded
|
||||
// into a page's title, so exchange the auth code for access & refresh tokens
|
||||
// and tell the window to close
|
||||
|
||||
// avoid duplicate signals that the callback point has been reached
|
||||
self.hasHandledCallback = YES;
|
||||
|
||||
[self closeTheWindow];
|
||||
|
||||
NSError *error = nil;
|
||||
|
||||
GTMOAuth2Authentication *auth = self.authentication;
|
||||
NSString *code = auth.code;
|
||||
if ([code length] > 0) {
|
||||
// exchange the code for a token
|
||||
SEL sel = @selector(auth:finishedWithFetcher:error:);
|
||||
GTMHTTPFetcher *fetcher = [auth beginTokenFetchWithDelegate:self
|
||||
didFinishSelector:sel];
|
||||
if (fetcher == nil) {
|
||||
error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
|
||||
code:-1
|
||||
userInfo:nil];
|
||||
} else {
|
||||
self.pendingFetcher = fetcher;
|
||||
}
|
||||
|
||||
// notify the app so it can put up a post-sign in, pre-token exchange UI
|
||||
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
||||
[nc postNotificationName:kGTMOAuth2UserSignedIn
|
||||
object:self
|
||||
userInfo:nil];
|
||||
} else {
|
||||
// the callback lacked an auth code
|
||||
NSString *errStr = auth.errorString;
|
||||
NSDictionary *userInfo = nil;
|
||||
if ([errStr length] > 0) {
|
||||
userInfo = [NSDictionary dictionaryWithObject:errStr
|
||||
forKey:kGTMOAuth2ErrorMessageKey];
|
||||
}
|
||||
|
||||
error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
|
||||
code:kGTMOAuth2ErrorAuthorizationFailed
|
||||
userInfo:userInfo];
|
||||
}
|
||||
|
||||
if (error) {
|
||||
[self finishSignInWithError:error];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)auth:(GTMOAuth2Authentication *)auth
|
||||
finishedWithFetcher:(GTMHTTPFetcher *)fetcher
|
||||
error:(NSError *)error {
|
||||
self.pendingFetcher = nil;
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
if (error == nil
|
||||
&& (self.shouldFetchGoogleUserEmail || self.shouldFetchGoogleUserProfile)
|
||||
&& [self.authentication.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) {
|
||||
// fetch the user's information from the Google server
|
||||
[self fetchGoogleUserInfo];
|
||||
} else {
|
||||
// we're not authorizing with Google, so we're done
|
||||
[self finishSignInWithError:error];
|
||||
}
|
||||
#else
|
||||
[self finishSignInWithError:error];
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
+ (GTMHTTPFetcher *)userInfoFetcherWithAuth:(GTMOAuth2Authentication *)auth {
|
||||
// create a fetcher for obtaining the user's email or profile
|
||||
NSURL *infoURL = [[self class] googleUserInfoURL];
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:infoURL];
|
||||
|
||||
NSString *userAgent = [auth userAgent];
|
||||
[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
|
||||
[request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
|
||||
|
||||
GTMHTTPFetcher *fetcher;
|
||||
id <GTMHTTPFetcherServiceProtocol> fetcherService = auth.fetcherService;
|
||||
if (fetcherService) {
|
||||
fetcher = [fetcherService fetcherWithRequest:request];
|
||||
} else {
|
||||
fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
|
||||
}
|
||||
fetcher.authorizer = auth;
|
||||
fetcher.retryEnabled = YES;
|
||||
fetcher.maxRetryInterval = 15.0;
|
||||
fetcher.comment = @"user info";
|
||||
return fetcher;
|
||||
}
|
||||
|
||||
- (void)fetchGoogleUserInfo {
|
||||
// fetch the user's email address or profile
|
||||
GTMOAuth2Authentication *auth = self.authentication;
|
||||
GTMHTTPFetcher *fetcher = [[self class] userInfoFetcherWithAuth:auth];
|
||||
[fetcher beginFetchWithDelegate:self
|
||||
didFinishSelector:@selector(infoFetcher:finishedWithData:error:)];
|
||||
|
||||
self.pendingFetcher = fetcher;
|
||||
|
||||
[auth notifyFetchIsRunning:YES
|
||||
fetcher:fetcher
|
||||
type:kGTMOAuth2FetchTypeUserInfo];
|
||||
}
|
||||
|
||||
- (void)infoFetcher:(GTMHTTPFetcher *)fetcher
|
||||
finishedWithData:(NSData *)data
|
||||
error:(NSError *)error {
|
||||
GTMOAuth2Authentication *auth = self.authentication;
|
||||
[auth notifyFetchIsRunning:NO
|
||||
fetcher:fetcher
|
||||
type:nil];
|
||||
|
||||
self.pendingFetcher = nil;
|
||||
|
||||
if (error) {
|
||||
#if DEBUG
|
||||
if (data) {
|
||||
NSString *dataStr = [[[NSString alloc] initWithData:data
|
||||
encoding:NSUTF8StringEncoding] autorelease];
|
||||
NSLog(@"infoFetcher error: %@\n%@", error, dataStr);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
// We have the authenticated user's info
|
||||
if (data) {
|
||||
NSDictionary *profileDict = [auth dictionaryWithJSONData:data];
|
||||
if (profileDict) {
|
||||
self.userProfile = profileDict;
|
||||
|
||||
// Save the email into the auth object
|
||||
NSString *email = [profileDict objectForKey:@"email"];
|
||||
[auth setUserEmail:email];
|
||||
|
||||
NSNumber *verified = [profileDict objectForKey:@"verified_email"];
|
||||
[auth setUserEmailIsVerified:[verified stringValue]];
|
||||
}
|
||||
}
|
||||
}
|
||||
[self finishSignInWithError:error];
|
||||
}
|
||||
|
||||
#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
|
||||
- (void)finishSignInWithError:(NSError *)error {
|
||||
[self invokeFinalCallbackWithError:error];
|
||||
}
|
||||
|
||||
// convenience method for making the final call to our delegate
|
||||
- (void)invokeFinalCallbackWithError:(NSError *)error {
|
||||
if (delegate_ && finishedSelector_) {
|
||||
GTMOAuth2Authentication *auth = self.authentication;
|
||||
|
||||
NSMethodSignature *sig = [delegate_ methodSignatureForSelector:finishedSelector_];
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
|
||||
[invocation setSelector:finishedSelector_];
|
||||
[invocation setTarget:delegate_];
|
||||
[invocation setArgument:&self atIndex:2];
|
||||
[invocation setArgument:&auth atIndex:3];
|
||||
[invocation setArgument:&error atIndex:4];
|
||||
[invocation invoke];
|
||||
}
|
||||
|
||||
// we'll no longer send messages to the delegate
|
||||
//
|
||||
// we want to autorelease it rather than assign to the property in case
|
||||
// the delegate is below us in the call stack
|
||||
[delegate_ autorelease];
|
||||
delegate_ = nil;
|
||||
}
|
||||
|
||||
#pragma mark Reachability monitoring
|
||||
|
||||
static void ReachabilityCallBack(SCNetworkReachabilityRef target,
|
||||
SCNetworkConnectionFlags flags,
|
||||
void *info) {
|
||||
// pass the flags to the signIn object
|
||||
GTMOAuth2SignIn *signIn = (GTMOAuth2SignIn *)info;
|
||||
|
||||
[signIn reachabilityTarget:target
|
||||
changedFlags:flags];
|
||||
}
|
||||
|
||||
- (void)startReachabilityCheck {
|
||||
// the user may set the timeout to 0 to skip the reachability checking
|
||||
// during display of the sign-in page
|
||||
if (networkLossTimeoutInterval_ <= 0.0 || reachabilityRef_ != NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// create a reachability target from the authorization URL, add our callback,
|
||||
// and schedule it on the run loop so we'll be notified if the network drops
|
||||
NSURL *url = self.authorizationURL;
|
||||
const char* host = [[url host] UTF8String];
|
||||
reachabilityRef_ = SCNetworkReachabilityCreateWithName(kCFAllocatorSystemDefault,
|
||||
host);
|
||||
if (reachabilityRef_) {
|
||||
BOOL isScheduled = NO;
|
||||
SCNetworkReachabilityContext ctx = { 0, self, NULL, NULL, NULL };
|
||||
|
||||
if (SCNetworkReachabilitySetCallback(reachabilityRef_,
|
||||
ReachabilityCallBack, &ctx)) {
|
||||
if (SCNetworkReachabilityScheduleWithRunLoop(reachabilityRef_,
|
||||
CFRunLoopGetCurrent(),
|
||||
kCFRunLoopDefaultMode)) {
|
||||
isScheduled = YES;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isScheduled) {
|
||||
CFRelease(reachabilityRef_);
|
||||
reachabilityRef_ = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)destroyUnreachabilityTimer {
|
||||
[networkLossTimer_ invalidate];
|
||||
[networkLossTimer_ autorelease];
|
||||
networkLossTimer_ = nil;
|
||||
}
|
||||
|
||||
- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef
|
||||
changedFlags:(SCNetworkConnectionFlags)flags {
|
||||
BOOL isConnected = (flags & kSCNetworkFlagsReachable) != 0
|
||||
&& (flags & kSCNetworkFlagsConnectionRequired) == 0;
|
||||
|
||||
if (isConnected) {
|
||||
// server is again reachable
|
||||
[self destroyUnreachabilityTimer];
|
||||
|
||||
if (hasNotifiedNetworkLoss_) {
|
||||
// tell the user that the network has been found
|
||||
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
||||
[nc postNotificationName:kGTMOAuth2NetworkFound
|
||||
object:self
|
||||
userInfo:nil];
|
||||
hasNotifiedNetworkLoss_ = NO;
|
||||
}
|
||||
} else {
|
||||
// the server has become unreachable; start the timer, if necessary
|
||||
if (networkLossTimer_ == nil
|
||||
&& networkLossTimeoutInterval_ > 0
|
||||
&& !hasNotifiedNetworkLoss_) {
|
||||
SEL sel = @selector(reachabilityTimerFired:);
|
||||
networkLossTimer_ = [[NSTimer scheduledTimerWithTimeInterval:networkLossTimeoutInterval_
|
||||
target:self
|
||||
selector:sel
|
||||
userInfo:nil
|
||||
repeats:NO] retain];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reachabilityTimerFired:(NSTimer *)timer {
|
||||
// the user may call [[notification object] cancelSigningIn] to
|
||||
// dismiss the sign-in
|
||||
if (!hasNotifiedNetworkLoss_) {
|
||||
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
||||
[nc postNotificationName:kGTMOAuth2NetworkLost
|
||||
object:self
|
||||
userInfo:nil];
|
||||
hasNotifiedNetworkLoss_ = YES;
|
||||
}
|
||||
|
||||
[self destroyUnreachabilityTimer];
|
||||
}
|
||||
|
||||
- (void)stopReachabilityCheck {
|
||||
[self destroyUnreachabilityTimer];
|
||||
|
||||
if (reachabilityRef_) {
|
||||
SCNetworkReachabilityUnscheduleFromRunLoop(reachabilityRef_,
|
||||
CFRunLoopGetCurrent(),
|
||||
kCFRunLoopDefaultMode);
|
||||
SCNetworkReachabilitySetCallback(reachabilityRef_, NULL, NULL);
|
||||
|
||||
CFRelease(reachabilityRef_);
|
||||
reachabilityRef_ = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Token Revocation
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
|
||||
if (auth.canAuthorize
|
||||
&& [auth.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) {
|
||||
|
||||
// create a signed revocation request for this authentication object
|
||||
NSURL *url = [self googleRevocationURL];
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
||||
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
|
||||
|
||||
NSString *token = auth.refreshToken;
|
||||
NSString *encoded = [GTMOAuth2Authentication encodedOAuthValueForString:token];
|
||||
NSString *body = [@"token=" stringByAppendingString:encoded];
|
||||
|
||||
[request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
[request setHTTPMethod:@"POST"];
|
||||
|
||||
NSString *userAgent = [auth userAgent];
|
||||
[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
|
||||
|
||||
// there's nothing to be done if revocation succeeds or fails
|
||||
GTMHTTPFetcher *fetcher;
|
||||
id <GTMHTTPFetcherServiceProtocol> fetcherService = auth.fetcherService;
|
||||
if (fetcherService) {
|
||||
fetcher = [fetcherService fetcherWithRequest:request];
|
||||
} else {
|
||||
fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
|
||||
}
|
||||
fetcher.comment = @"revoke token";
|
||||
|
||||
// Use a completion handler fetch for better debugging, but only if we're
|
||||
// guaranteed that blocks are available in the runtime
|
||||
#if (!TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MIN_REQUIRED >= 1060)) || \
|
||||
(TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000))
|
||||
// Blocks are available
|
||||
[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
|
||||
#if DEBUG
|
||||
if (error) {
|
||||
NSString *errStr = [[[NSString alloc] initWithData:data
|
||||
encoding:NSUTF8StringEncoding] autorelease];
|
||||
NSLog(@"revoke error: %@", errStr);
|
||||
}
|
||||
#endif // DEBUG
|
||||
}];
|
||||
#else
|
||||
// Blocks may not be available
|
||||
[fetcher beginFetchWithDelegate:nil didFinishSelector:NULL];
|
||||
#endif
|
||||
}
|
||||
|
||||
[auth reset];
|
||||
}
|
||||
#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
|
||||
@end
|
||||
|
||||
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|
||||
333
OAuth 2/GTMOAuth2WindowController.h
Executable file
333
OAuth 2/GTMOAuth2WindowController.h
Executable file
@@ -0,0 +1,333 @@
|
||||
/* Copyright (c) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// GTMOAuth2WindowController
|
||||
//
|
||||
// This window controller for Mac handles sign-in via OAuth2 to Google or
|
||||
// other services.
|
||||
//
|
||||
// This controller is not reusable; create a new instance of this controller
|
||||
// every time the user will sign in.
|
||||
//
|
||||
// Sample usage for signing in to a Google service:
|
||||
//
|
||||
// static NSString *const kKeychainItemName = @”My App: Google Plus”;
|
||||
// NSString *scope = @"https://www.googleapis.com/auth/plus.me";
|
||||
//
|
||||
//
|
||||
// GTMOAuth2WindowController *windowController;
|
||||
// windowController = [[[GTMOAuth2WindowController alloc] initWithScope:scope
|
||||
// clientID:clientID
|
||||
// clientSecret:clientSecret
|
||||
// keychainItemName:kKeychainItemName
|
||||
// resourceBundle:nil] autorelease];
|
||||
//
|
||||
// [windowController signInSheetModalForWindow:mMainWindow
|
||||
// delegate:self
|
||||
// finishedSelector:@selector(windowController:finishedWithAuth:error:)];
|
||||
//
|
||||
// The finished selector should have a signature matching this:
|
||||
//
|
||||
// - (void)windowController:(GTMOAuth2WindowController *)windowController
|
||||
// finishedWithAuth:(GTMOAuth2Authentication *)auth
|
||||
// error:(NSError *)error {
|
||||
// if (error != nil) {
|
||||
// // sign in failed
|
||||
// } else {
|
||||
// // sign in succeeded
|
||||
// //
|
||||
// // with the GTL library, pass the authentication to the service object,
|
||||
// // like
|
||||
// // [[self contactService] setAuthorizer:auth];
|
||||
// //
|
||||
// // or use it to sign a request directly, like
|
||||
// // BOOL isAuthorizing = [self authorizeRequest:request
|
||||
// // delegate:self
|
||||
// // didFinishSelector:@selector(auth:finishedWithError:)];
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// To sign in to services other than Google, use the longer init method,
|
||||
// as shown in the sample application
|
||||
//
|
||||
// If the network connection is lost for more than 30 seconds while the sign-in
|
||||
// html is displayed, the notification kGTLOAuthNetworkLost will be sent.
|
||||
|
||||
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
// GTMHTTPFetcher.h brings in GTLDefines/GDataDefines
|
||||
#import "GTMHTTPFetcher.h"
|
||||
|
||||
#import "GTMOAuth2SignIn.h"
|
||||
#import "GTMOAuth2Authentication.h"
|
||||
#import "GTMHTTPFetchHistory.h" // for GTMCookieStorage
|
||||
|
||||
@class GTMOAuth2SignIn;
|
||||
|
||||
@interface GTMOAuth2WindowController : NSWindowController {
|
||||
@private
|
||||
// IBOutlets
|
||||
NSButton *keychainCheckbox_;
|
||||
WebView *webView_;
|
||||
NSButton *webCloseButton_;
|
||||
NSButton *webBackButton_;
|
||||
|
||||
// the object responsible for the sign-in networking sequence; it holds
|
||||
// onto the authentication object as well
|
||||
GTMOAuth2SignIn *signIn_;
|
||||
|
||||
// the page request to load when awakeFromNib occurs
|
||||
NSURLRequest *initialRequest_;
|
||||
|
||||
// local storage for WebKit cookies so they're not shared with Safari
|
||||
GTMCookieStorage *cookieStorage_;
|
||||
|
||||
// the user we're calling back
|
||||
//
|
||||
// the delegate is retained only until the callback is invoked
|
||||
// or the sign-in is canceled
|
||||
id delegate_;
|
||||
SEL finishedSelector_;
|
||||
|
||||
#if NS_BLOCKS_AVAILABLE
|
||||
void (^completionBlock_)(GTMOAuth2Authentication *, NSError *);
|
||||
#elif !__LP64__
|
||||
// placeholders: for 32-bit builds, keep the size of the object's ivar section
|
||||
// the same with and without blocks
|
||||
#ifndef __clang_analyzer__
|
||||
id completionPlaceholder_;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// flag allowing application to quit during display of sign-in sheet on 10.6
|
||||
// and later
|
||||
BOOL shouldAllowApplicationTermination_;
|
||||
|
||||
// delegate method for handling URLs to be opened in external windows
|
||||
SEL externalRequestSelector_;
|
||||
|
||||
BOOL isWindowShown_;
|
||||
|
||||
// paranoid flag to ensure we only close once during the sign-in sequence
|
||||
BOOL hasDoneFinalRedirect_;
|
||||
|
||||
// paranoid flag to ensure we only call the user back once
|
||||
BOOL hasCalledFinished_;
|
||||
|
||||
// if non-nil, we display as a sheet on the specified window
|
||||
NSWindow *sheetModalForWindow_;
|
||||
|
||||
// if non-empty, the name of the application and service used for the
|
||||
// keychain item
|
||||
NSString *keychainItemName_;
|
||||
|
||||
// if non-nil, the html string to be displayed immediately upon opening
|
||||
// of the web view
|
||||
NSString *initialHTMLString_;
|
||||
|
||||
// if true, we allow default WebView handling of cookies, so the
|
||||
// same user remains signed in each time the dialog is displayed
|
||||
BOOL shouldPersistUser_;
|
||||
|
||||
// user-defined data
|
||||
id userData_;
|
||||
NSMutableDictionary *properties_;
|
||||
}
|
||||
|
||||
// User interface elements
|
||||
@property (nonatomic, assign) IBOutlet NSButton *keychainCheckbox;
|
||||
@property (nonatomic, assign) IBOutlet WebView *webView;
|
||||
@property (nonatomic, assign) IBOutlet NSButton *webCloseButton;
|
||||
@property (nonatomic, assign) IBOutlet NSButton *webBackButton;
|
||||
|
||||
// The application and service name to use for saving the auth tokens
|
||||
// to the keychain
|
||||
@property (nonatomic, copy) NSString *keychainItemName;
|
||||
|
||||
// If true, the sign-in will remember which user was last signed in
|
||||
//
|
||||
// Defaults to false, so showing the sign-in window will always ask for
|
||||
// the username and password, rather than skip to the grant authorization
|
||||
// page. During development, it may be convenient to set this to true
|
||||
// to speed up signing in.
|
||||
@property (nonatomic, assign) BOOL shouldPersistUser;
|
||||
|
||||
// Optional html string displayed immediately upon opening the web view
|
||||
//
|
||||
// This string is visible just until the sign-in web page loads, and
|
||||
// may be used for a "Loading..." type of message
|
||||
@property (nonatomic, copy) NSString *initialHTMLString;
|
||||
|
||||
// The default timeout for an unreachable network during display of the
|
||||
// sign-in page is 30 seconds, after which the notification
|
||||
// kGTLOAuthNetworkLost is sent; set this to 0 to have no timeout
|
||||
@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval;
|
||||
|
||||
// On 10.6 and later, the sheet can allow application termination by calling
|
||||
// NSWindow's setPreventsApplicationTerminationWhenModal:
|
||||
@property (nonatomic, assign) BOOL shouldAllowApplicationTermination;
|
||||
|
||||
// Selector for a delegate method to handle requests sent to an external
|
||||
// browser.
|
||||
//
|
||||
// Selector should have a signature matching
|
||||
// - (void)windowController:(GTMOAuth2WindowController *)controller
|
||||
// opensRequest:(NSURLRequest *)request;
|
||||
//
|
||||
// The controller's default behavior is to use NSWorkspace's openURL:
|
||||
@property (nonatomic, assign) SEL externalRequestSelector;
|
||||
|
||||
// The underlying object to hold authentication tokens and authorize http
|
||||
// requests
|
||||
@property (nonatomic, retain, readonly) GTMOAuth2Authentication *authentication;
|
||||
|
||||
// The underlying object which performs the sign-in networking sequence
|
||||
@property (nonatomic, retain, readonly) GTMOAuth2SignIn *signIn;
|
||||
|
||||
// Any arbitrary data object the user would like the controller to retain
|
||||
@property (nonatomic, retain) id userData;
|
||||
|
||||
// Stored property values are retained for the convenience of the caller
|
||||
- (void)setProperty:(id)obj forKey:(NSString *)key;
|
||||
- (id)propertyForKey:(NSString *)key;
|
||||
|
||||
@property (nonatomic, retain) NSDictionary *properties;
|
||||
|
||||
- (IBAction)closeWindow:(id)sender;
|
||||
- (IBAction)toggleStorePasswordInKeychain:(id)sender;
|
||||
|
||||
// Create a controller for authenticating to Google services
|
||||
//
|
||||
// scope is the requested scope of authorization
|
||||
// (like "http://www.google.com/m8/feeds")
|
||||
//
|
||||
// keychainItemName is used for storing the token on the keychain,
|
||||
// and is required for the "remember for later" checkbox to be shown;
|
||||
// keychainItemName should be like "My Application: Google Contacts"
|
||||
// (or set to nil if no persistent keychain storage is desired)
|
||||
//
|
||||
// resourceBundle may be nil if the window is in the main bundle's nib
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
+ (id)controllerWithScope:(NSString *)scope
|
||||
clientID:(NSString *)clientID
|
||||
clientSecret:(NSString *)clientSecret
|
||||
keychainItemName:(NSString *)keychainItemName // may be nil
|
||||
resourceBundle:(NSBundle *)bundle; // may be nil
|
||||
|
||||
- (id)initWithScope:(NSString *)scope
|
||||
clientID:(NSString *)clientID
|
||||
clientSecret:(NSString *)clientSecret
|
||||
keychainItemName:(NSString *)keychainItemName
|
||||
resourceBundle:(NSBundle *)bundle;
|
||||
#endif
|
||||
|
||||
// Create a controller for authenticating to non-Google services, taking
|
||||
// explicit endpoint URLs and an authentication object
|
||||
+ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
|
||||
authorizationURL:(NSURL *)authorizationURL
|
||||
keychainItemName:(NSString *)keychainItemName // may be nil
|
||||
resourceBundle:(NSBundle *)bundle; // may be nil
|
||||
|
||||
// This is the designated initializer
|
||||
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
|
||||
authorizationURL:(NSURL *)authorizationURL
|
||||
keychainItemName:(NSString *)keychainItemName
|
||||
resourceBundle:(NSBundle *)bundle;
|
||||
|
||||
// Entry point to begin displaying the sign-in window
|
||||
//
|
||||
// the finished selector should have a signature matching
|
||||
// - (void)windowController:(GTMOAuth2WindowController *)windowController
|
||||
// finishedWithAuth:(GTMOAuth2Authentication *)auth
|
||||
// error:(NSError *)error {
|
||||
//
|
||||
// Once the finished method has been invoked with no error, the auth object
|
||||
// may be used to authorize requests (refreshing the access token, if necessary,
|
||||
// and adding the auth header) like:
|
||||
//
|
||||
// [authorizer authorizeRequest:myNSMutableURLRequest]
|
||||
// delegate:self
|
||||
// didFinishSelector:@selector(auth:finishedWithError:)];
|
||||
//
|
||||
// or can be stored in a GTL service object like
|
||||
// GTLServiceGoogleContact *service = [self contactService];
|
||||
// [service setAuthorizer:auth];
|
||||
//
|
||||
// The delegate is retained only until the finished selector is invoked or
|
||||
// the sign-in is canceled
|
||||
- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
|
||||
delegate:(id)delegate
|
||||
finishedSelector:(SEL)finishedSelector;
|
||||
|
||||
#if NS_BLOCKS_AVAILABLE
|
||||
- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
|
||||
completionHandler:(void (^)(GTMOAuth2Authentication *auth, NSError *error))handler;
|
||||
#endif
|
||||
|
||||
- (void)cancelSigningIn;
|
||||
|
||||
// Subclasses may override authNibName to specify a custom name
|
||||
+ (NSString *)authNibName;
|
||||
|
||||
// apps may replace the sign-in class with their own subclass of it
|
||||
+ (Class)signInClass;
|
||||
+ (void)setSignInClass:(Class)theClass;
|
||||
|
||||
// Revocation of an authorized token from Google
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth;
|
||||
#endif
|
||||
|
||||
// Keychain
|
||||
//
|
||||
// The keychain checkbox is shown if the keychain application service
|
||||
// name (typically set in the initWithScope: method) is non-empty
|
||||
//
|
||||
|
||||
// Create an authentication object for Google services from the access
|
||||
// token and secret stored in the keychain; if no token is available, return
|
||||
// an unauthorized auth object
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
+ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
|
||||
clientID:(NSString *)clientID
|
||||
clientSecret:(NSString *)clientSecret;
|
||||
#endif
|
||||
|
||||
// Add tokens from the keychain, if available, to the authentication object
|
||||
//
|
||||
// returns YES if the authentication object was authorized from the keychain
|
||||
+ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName
|
||||
authentication:(GTMOAuth2Authentication *)auth;
|
||||
|
||||
// Method for deleting the stored access token and secret, useful for "signing
|
||||
// out"
|
||||
+ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName;
|
||||
|
||||
// Method for saving the stored access token and secret; typically, this method
|
||||
// is used only by the window controller
|
||||
+ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName
|
||||
authentication:(GTMOAuth2Authentication *)auth;
|
||||
@end
|
||||
|
||||
#endif // #if !TARGET_OS_IPHONE
|
||||
|
||||
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|
||||
738
OAuth 2/GTMOAuth2WindowController.m
Executable file
738
OAuth 2/GTMOAuth2WindowController.m
Executable file
@@ -0,0 +1,738 @@
|
||||
/* Copyright (c) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
|
||||
#import "GTMOAuth2WindowController.h"
|
||||
|
||||
@interface GTMOAuth2WindowController ()
|
||||
@property (nonatomic, retain) GTMOAuth2SignIn *signIn;
|
||||
@property (nonatomic, copy) NSURLRequest *initialRequest;
|
||||
@property (nonatomic, retain) GTMCookieStorage *cookieStorage;
|
||||
@property (nonatomic, retain) NSWindow *sheetModalForWindow;
|
||||
|
||||
- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil;
|
||||
- (void)setupSheetTerminationHandling;
|
||||
- (void)destroyWindow;
|
||||
- (void)handlePrematureWindowClose;
|
||||
- (BOOL)shouldUseKeychain;
|
||||
- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request;
|
||||
- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error;
|
||||
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
|
||||
|
||||
- (void)handleCookiesForResponse:(NSURLResponse *)response;
|
||||
- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request;
|
||||
@end
|
||||
|
||||
const char *kKeychainAccountName = "OAuth";
|
||||
|
||||
@implementation GTMOAuth2WindowController
|
||||
|
||||
// IBOutlets
|
||||
@synthesize keychainCheckbox = keychainCheckbox_,
|
||||
webView = webView_,
|
||||
webCloseButton = webCloseButton_,
|
||||
webBackButton = webBackButton_;
|
||||
|
||||
// regular ivars
|
||||
@synthesize signIn = signIn_,
|
||||
initialRequest = initialRequest_,
|
||||
cookieStorage = cookieStorage_,
|
||||
sheetModalForWindow = sheetModalForWindow_,
|
||||
keychainItemName = keychainItemName_,
|
||||
initialHTMLString = initialHTMLString_,
|
||||
shouldAllowApplicationTermination = shouldAllowApplicationTermination_,
|
||||
externalRequestSelector = externalRequestSelector_,
|
||||
shouldPersistUser = shouldPersistUser_,
|
||||
userData = userData_,
|
||||
properties = properties_;
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
// Create a controller for authenticating to Google services
|
||||
+ (id)controllerWithScope:(NSString *)scope
|
||||
clientID:(NSString *)clientID
|
||||
clientSecret:(NSString *)clientSecret
|
||||
keychainItemName:(NSString *)keychainItemName
|
||||
resourceBundle:(NSBundle *)bundle {
|
||||
return [[[self alloc] initWithScope:scope
|
||||
clientID:clientID
|
||||
clientSecret:clientSecret
|
||||
keychainItemName:keychainItemName
|
||||
resourceBundle:bundle] autorelease];
|
||||
}
|
||||
|
||||
- (id)initWithScope:(NSString *)scope
|
||||
clientID:(NSString *)clientID
|
||||
clientSecret:(NSString *)clientSecret
|
||||
keychainItemName:(NSString *)keychainItemName
|
||||
resourceBundle:(NSBundle *)bundle {
|
||||
Class signInClass = [[self class] signInClass];
|
||||
GTMOAuth2Authentication *auth;
|
||||
auth = [signInClass standardGoogleAuthenticationForScope:scope
|
||||
clientID:clientID
|
||||
clientSecret:clientSecret];
|
||||
NSURL *authorizationURL = [signInClass googleAuthorizationURL];
|
||||
return [self initWithAuthentication:auth
|
||||
authorizationURL:authorizationURL
|
||||
keychainItemName:keychainItemName
|
||||
resourceBundle:bundle];
|
||||
}
|
||||
#endif
|
||||
|
||||
// Create a controller for authenticating to any service
|
||||
+ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
|
||||
authorizationURL:(NSURL *)authorizationURL
|
||||
keychainItemName:(NSString *)keychainItemName
|
||||
resourceBundle:(NSBundle *)bundle {
|
||||
return [[[self alloc] initWithAuthentication:auth
|
||||
authorizationURL:authorizationURL
|
||||
keychainItemName:keychainItemName
|
||||
resourceBundle:bundle] autorelease];
|
||||
}
|
||||
|
||||
- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
|
||||
authorizationURL:(NSURL *)authorizationURL
|
||||
keychainItemName:(NSString *)keychainItemName
|
||||
resourceBundle:(NSBundle *)bundle {
|
||||
if (bundle == nil) {
|
||||
bundle = [NSBundle mainBundle];
|
||||
}
|
||||
|
||||
NSString *nibName = [[self class] authNibName];
|
||||
NSString *nibPath = [bundle pathForResource:nibName
|
||||
ofType:@"nib"];
|
||||
self = [super initWithWindowNibPath:nibPath
|
||||
owner:self];
|
||||
if (self != nil) {
|
||||
// use the supplied auth and OAuth endpoint URLs
|
||||
Class signInClass = [[self class] signInClass];
|
||||
signIn_ = [[signInClass alloc] initWithAuthentication:auth
|
||||
authorizationURL:authorizationURL
|
||||
delegate:self
|
||||
webRequestSelector:@selector(signIn:displayRequest:)
|
||||
finishedSelector:@selector(signIn:finishedWithAuth:error:)];
|
||||
keychainItemName_ = [keychainItemName copy];
|
||||
|
||||
// create local, temporary storage for WebKit cookies
|
||||
cookieStorage_ = [[GTMCookieStorage alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[signIn_ release];
|
||||
[initialRequest_ release];
|
||||
[cookieStorage_ release];
|
||||
[delegate_ release];
|
||||
#if NS_BLOCKS_AVAILABLE
|
||||
[completionBlock_ release];
|
||||
#endif
|
||||
[sheetModalForWindow_ release];
|
||||
[keychainItemName_ release];
|
||||
[initialHTMLString_ release];
|
||||
[userData_ release];
|
||||
[properties_ release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (void)awakeFromNib {
|
||||
// load the requested initial sign-in page
|
||||
[self.webView setResourceLoadDelegate:self];
|
||||
[self.webView setPolicyDelegate:self];
|
||||
|
||||
// the app may prefer some html other than blank white to be displayed
|
||||
// before the sign-in web page loads
|
||||
NSString *html = self.initialHTMLString;
|
||||
if ([html length] > 0) {
|
||||
[[self.webView mainFrame] loadHTMLString:html baseURL:nil];
|
||||
}
|
||||
|
||||
// hide the keychain checkbox if we're not supporting keychain
|
||||
BOOL hideKeychainCheckbox = ![self shouldUseKeychain];
|
||||
|
||||
const NSTimeInterval kJanuary2011 = 1293840000;
|
||||
BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011);
|
||||
if (isDateValid) {
|
||||
// start the asynchronous load of the sign-in web page
|
||||
[[self.webView mainFrame] performSelector:@selector(loadRequest:)
|
||||
withObject:self.initialRequest
|
||||
afterDelay:0.01
|
||||
inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
|
||||
} else {
|
||||
// clock date is invalid, so signing in would fail with an unhelpful error
|
||||
// from the server. Warn the user in an html string showing a watch icon,
|
||||
// question mark, and the system date and time. Hopefully this will clue
|
||||
// in brighter users, or at least let them make a useful screenshot to show
|
||||
// to developers.
|
||||
//
|
||||
// Even better is for apps to check the system clock and show some more
|
||||
// helpful, localized instructions for users; this is really a fallback.
|
||||
NSString *htmlTemplate = @"<html><body><div align=center><font size='7'>"
|
||||
@"⌚ ?<br><i>System Clock Incorrect</i><br>%@"
|
||||
@"</font></div></body></html>";
|
||||
NSString *errHTML = [NSString stringWithFormat:htmlTemplate, [NSDate date]];
|
||||
|
||||
[[webView_ mainFrame] loadHTMLString:errHTML baseURL:nil];
|
||||
hideKeychainCheckbox = YES;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
// Verify that Javascript is enabled
|
||||
BOOL hasJS = [[webView_ preferences] isJavaScriptEnabled];
|
||||
NSAssert(hasJS, @"GTMOAuth2: Javascript is required");
|
||||
#endif
|
||||
|
||||
[keychainCheckbox_ setHidden:hideKeychainCheckbox];
|
||||
}
|
||||
|
||||
+ (NSString *)authNibName {
|
||||
// subclasses may override this to specify a custom nib name
|
||||
return @"GTMOAuth2Window";
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
|
||||
delegate:(id)delegate
|
||||
finishedSelector:(SEL)finishedSelector {
|
||||
// check the selector on debug builds
|
||||
GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector,
|
||||
@encode(GTMOAuth2WindowController *), @encode(GTMOAuth2Authentication *),
|
||||
@encode(NSError *), 0);
|
||||
|
||||
delegate_ = [delegate retain];
|
||||
finishedSelector_ = finishedSelector;
|
||||
|
||||
[self signInCommonForWindow:parentWindowOrNil];
|
||||
}
|
||||
|
||||
#if NS_BLOCKS_AVAILABLE
|
||||
- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
|
||||
completionHandler:(void (^)(GTMOAuth2Authentication *, NSError *))handler {
|
||||
completionBlock_ = [handler copy];
|
||||
|
||||
[self signInCommonForWindow:parentWindowOrNil];
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil {
|
||||
self.sheetModalForWindow = parentWindowOrNil;
|
||||
hasDoneFinalRedirect_ = NO;
|
||||
hasCalledFinished_ = NO;
|
||||
|
||||
[self.signIn startSigningIn];
|
||||
}
|
||||
|
||||
- (void)cancelSigningIn {
|
||||
// The user has explicitly asked us to cancel signing in
|
||||
// (so no further callback is required)
|
||||
hasCalledFinished_ = YES;
|
||||
|
||||
[delegate_ autorelease];
|
||||
delegate_ = nil;
|
||||
|
||||
#if NS_BLOCKS_AVAILABLE
|
||||
[completionBlock_ autorelease];
|
||||
completionBlock_ = nil;
|
||||
#endif
|
||||
|
||||
// The signIn object's cancel method will close the window
|
||||
[self.signIn cancelSigningIn];
|
||||
hasDoneFinalRedirect_ = YES;
|
||||
}
|
||||
|
||||
- (IBAction)closeWindow:(id)sender {
|
||||
// dismiss the window/sheet before we call back the client
|
||||
[self destroyWindow];
|
||||
[self handlePrematureWindowClose];
|
||||
}
|
||||
|
||||
- (IBAction)toggleStorePasswordInKeychain:(id)sender {
|
||||
if ([sender state] == NSOffState) {
|
||||
NSBeginAlertSheet(NSLocalizedString(@"Do you want to disable saving your login data in your keychain?", nil), NSLocalizedString(@"Keep enabled", nil), NSLocalizedString(@"Disable", nil), nil, self.window, self, @selector(disableKeychainSheetDidEnd:returnCode:contextInfo:), nil, NULL, NSLocalizedString(@"If you do not save the password you will have to sign in every time you use this feature.", nil));
|
||||
}
|
||||
}
|
||||
|
||||
- (void)disableKeychainSheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
|
||||
{
|
||||
if (returnCode == NSAlertDefaultReturn) {
|
||||
self.keychainCheckbox.state = NSOnState;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark SignIn callbacks
|
||||
|
||||
- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request {
|
||||
// this is the signIn object's webRequest method, telling the controller
|
||||
// to either display the request in the webview, or close the window
|
||||
//
|
||||
// All web requests and all window closing goes through this routine
|
||||
|
||||
#if DEBUG
|
||||
if ((isWindowShown_ && request != nil)
|
||||
|| (!isWindowShown_ && request == nil)) {
|
||||
NSLog(@"Window state unexpected for request %@", [request URL]);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (request != nil) {
|
||||
// display the request
|
||||
self.initialRequest = request;
|
||||
|
||||
NSWindow *parentWindow = self.sheetModalForWindow;
|
||||
if (parentWindow) {
|
||||
[self setupSheetTerminationHandling];
|
||||
|
||||
NSWindow *sheet = [self window];
|
||||
[NSApp beginSheet:sheet
|
||||
modalForWindow:parentWindow
|
||||
modalDelegate:self
|
||||
didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
|
||||
contextInfo:nil];
|
||||
} else {
|
||||
// modeless
|
||||
[self showWindow:self];
|
||||
}
|
||||
isWindowShown_ = YES;
|
||||
} else {
|
||||
// request was nil
|
||||
[self destroyWindow];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupSheetTerminationHandling {
|
||||
NSWindow *sheet = [self window];
|
||||
|
||||
SEL sel = @selector(setPreventsApplicationTerminationWhenModal:);
|
||||
if ([sheet respondsToSelector:sel]) {
|
||||
// setPreventsApplicationTerminationWhenModal is available in NSWindow
|
||||
// on 10.6 and later
|
||||
BOOL boolVal = !self.shouldAllowApplicationTermination;
|
||||
NSMethodSignature *sig = [sheet methodSignatureForSelector:sel];
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
|
||||
[invocation setSelector:sel];
|
||||
[invocation setTarget:sheet];
|
||||
[invocation setArgument:&boolVal atIndex:2];
|
||||
[invocation invoke];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)destroyWindow {
|
||||
// no request; close the window (but not immediately, in case
|
||||
// we're called in response to some window event)
|
||||
NSWindow *parentWindow = self.sheetModalForWindow;
|
||||
if (parentWindow) {
|
||||
[NSApp endSheet:[self window]];
|
||||
} else {
|
||||
[[self window] performSelector:@selector(close)
|
||||
withObject:nil
|
||||
afterDelay:0.1
|
||||
inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
|
||||
|
||||
// Avoid more callbacks after the delayed close happens, as the window
|
||||
// controller may be gone.
|
||||
[[self webView] setResourceLoadDelegate:nil];
|
||||
}
|
||||
isWindowShown_ = NO;
|
||||
}
|
||||
|
||||
- (void)handlePrematureWindowClose {
|
||||
if (!hasDoneFinalRedirect_) {
|
||||
// tell the sign-in object to tell the user's finished method
|
||||
// that we're done
|
||||
[self.signIn windowWasClosed];
|
||||
hasDoneFinalRedirect_ = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
|
||||
[sheet orderOut:self];
|
||||
|
||||
self.sheetModalForWindow = nil;
|
||||
}
|
||||
|
||||
- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error {
|
||||
if (!hasCalledFinished_) {
|
||||
hasCalledFinished_ = YES;
|
||||
|
||||
if (error == nil) {
|
||||
BOOL shouldUseKeychain = [self shouldUseKeychain];
|
||||
if (shouldUseKeychain) {
|
||||
BOOL canAuthorize = auth.canAuthorize;
|
||||
BOOL isKeychainChecked = ([keychainCheckbox_ state] == NSOnState);
|
||||
|
||||
NSString *keychainItemName = self.keychainItemName;
|
||||
|
||||
if (isKeychainChecked && canAuthorize) {
|
||||
// save the auth params in the keychain
|
||||
[[self class] saveAuthToKeychainForName:keychainItemName
|
||||
authentication:auth];
|
||||
} else {
|
||||
// remove the auth params from the keychain
|
||||
[[self class] removeAuthFromKeychainForName:keychainItemName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (delegate_ && finishedSelector_) {
|
||||
SEL sel = finishedSelector_;
|
||||
NSMethodSignature *sig = [delegate_ methodSignatureForSelector:sel];
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
|
||||
[invocation setSelector:sel];
|
||||
[invocation setTarget:delegate_];
|
||||
[invocation setArgument:&self atIndex:2];
|
||||
[invocation setArgument:&auth atIndex:3];
|
||||
[invocation setArgument:&error atIndex:4];
|
||||
[invocation invoke];
|
||||
}
|
||||
|
||||
[delegate_ autorelease];
|
||||
delegate_ = nil;
|
||||
|
||||
#if NS_BLOCKS_AVAILABLE
|
||||
if (completionBlock_) {
|
||||
completionBlock_(auth, error);
|
||||
|
||||
// release the block here to avoid a retain loop on the controller
|
||||
[completionBlock_ autorelease];
|
||||
completionBlock_ = nil;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static Class gSignInClass = Nil;
|
||||
|
||||
+ (Class)signInClass {
|
||||
if (gSignInClass == Nil) {
|
||||
gSignInClass = [GTMOAuth2SignIn class];
|
||||
}
|
||||
return gSignInClass;
|
||||
}
|
||||
|
||||
+ (void)setSignInClass:(Class)theClass {
|
||||
gSignInClass = theClass;
|
||||
}
|
||||
|
||||
#pragma mark Token Revocation
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
|
||||
[[self signInClass] revokeTokenForGoogleAuthentication:auth];
|
||||
}
|
||||
#endif
|
||||
|
||||
#pragma mark WebView methods
|
||||
|
||||
- (NSURLRequest *)webView:(WebView *)sender resource:(id)identifier willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse fromDataSource:(WebDataSource *)dataSource {
|
||||
// override WebKit's cookie storage with our own to avoid cookie persistence
|
||||
// across sign-ins and interaction with the Safari browser's sign-in state
|
||||
[self handleCookiesForResponse:redirectResponse];
|
||||
request = [self addCookiesToRequest:request];
|
||||
|
||||
if (!hasDoneFinalRedirect_) {
|
||||
hasDoneFinalRedirect_ = [self.signIn requestRedirectedToRequest:request];
|
||||
if (hasDoneFinalRedirect_) {
|
||||
// signIn has told the window to close
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
- (void)webView:(WebView *)sender resource:(id)identifier didReceiveResponse:(NSURLResponse *)response fromDataSource:(WebDataSource *)dataSource {
|
||||
// override WebKit's cookie storage with our own
|
||||
[self handleCookiesForResponse:response];
|
||||
}
|
||||
|
||||
- (void)webView:(WebView *)sender resource:(id)identifier didFinishLoadingFromDataSource:(WebDataSource *)dataSource {
|
||||
NSString *title = [sender stringByEvaluatingJavaScriptFromString:@"document.title"];
|
||||
if ([title length] > 0) {
|
||||
[self.signIn titleChanged:title];
|
||||
}
|
||||
|
||||
[signIn_ cookiesChanged:(NSHTTPCookieStorage *)cookieStorage_];
|
||||
}
|
||||
|
||||
- (void)webView:(WebView *)sender resource:(id)identifier didFailLoadingWithError:(NSError *)error fromDataSource:(WebDataSource *)dataSource {
|
||||
[self.signIn loadFailedWithError:error];
|
||||
}
|
||||
|
||||
- (void)windowWillClose:(NSNotification *)note {
|
||||
if (isWindowShown_) {
|
||||
[self handlePrematureWindowClose];
|
||||
}
|
||||
isWindowShown_ = NO;
|
||||
}
|
||||
|
||||
- (void)webView:(WebView *)webView
|
||||
decidePolicyForNewWindowAction:(NSDictionary *)actionInformation
|
||||
request:(NSURLRequest *)request
|
||||
newFrameName:(NSString *)frameName
|
||||
decisionListener:(id<WebPolicyDecisionListener>)listener {
|
||||
SEL sel = self.externalRequestSelector;
|
||||
if (sel) {
|
||||
[delegate_ performSelector:sel
|
||||
withObject:self
|
||||
withObject:request];
|
||||
} else {
|
||||
// default behavior is to open the URL in NSWorkspace's default browser
|
||||
NSURL *url = [request URL];
|
||||
[[NSWorkspace sharedWorkspace] openURL:url];
|
||||
}
|
||||
[listener ignore];
|
||||
}
|
||||
|
||||
#pragma mark Cookie management
|
||||
|
||||
// Rather than let the WebView use Safari's default cookie storage, we intercept
|
||||
// requests and response to segregate and later discard cookies from signing in.
|
||||
//
|
||||
// This allows the application to actually sign out by discarding the auth token
|
||||
// rather than the user being kept signed in by the cookies.
|
||||
|
||||
- (void)handleCookiesForResponse:(NSURLResponse *)response {
|
||||
if (self.shouldPersistUser) {
|
||||
// we'll let WebKit handle the cookies; they'll persist across apps
|
||||
// and across runs of this app
|
||||
return;
|
||||
}
|
||||
|
||||
if ([response respondsToSelector:@selector(allHeaderFields)]) {
|
||||
// grab the cookies from the header as NSHTTPCookies and store them locally
|
||||
NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
|
||||
if (headers) {
|
||||
NSURL *url = [response URL];
|
||||
NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers
|
||||
forURL:url];
|
||||
if ([cookies count] > 0) {
|
||||
[cookieStorage_ setCookies:cookies];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request {
|
||||
if (self.shouldPersistUser) {
|
||||
// we'll let WebKit handle the cookies; they'll persist across apps
|
||||
// and across runs of this app
|
||||
return request;
|
||||
}
|
||||
|
||||
// override WebKit's usual automatic storage of cookies
|
||||
NSMutableURLRequest *mutableRequest = [[request mutableCopy] autorelease];
|
||||
[mutableRequest setHTTPShouldHandleCookies:NO];
|
||||
|
||||
// add our locally-stored cookies for this URL, if any
|
||||
NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]];
|
||||
if ([cookies count] > 0) {
|
||||
NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
|
||||
NSString *cookieHeader = [headers objectForKey:@"Cookie"];
|
||||
if (cookieHeader) {
|
||||
[mutableRequest setValue:cookieHeader forHTTPHeaderField:@"Cookie"];
|
||||
}
|
||||
}
|
||||
return mutableRequest;
|
||||
}
|
||||
|
||||
#pragma mark Keychain support
|
||||
|
||||
+ (NSString *)prefsKeyForName:(NSString *)keychainItemName {
|
||||
NSString *result = [@"OAuth2: " stringByAppendingString:keychainItemName];
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName
|
||||
authentication:(GTMOAuth2Authentication *)auth {
|
||||
|
||||
[self removeAuthFromKeychainForName:keychainItemName];
|
||||
|
||||
// don't save unless we have a token that can really authorize requests
|
||||
if (!auth.canAuthorize) return NO;
|
||||
|
||||
// make a response string containing the values we want to save
|
||||
NSString *password = [auth persistenceResponseString];
|
||||
|
||||
SecKeychainRef defaultKeychain = NULL;
|
||||
SecKeychainItemRef *dontWantItemRef= NULL;
|
||||
const char *utf8ServiceName = [keychainItemName UTF8String];
|
||||
const char *utf8Password = [password UTF8String];
|
||||
|
||||
OSStatus err = SecKeychainAddGenericPassword(defaultKeychain,
|
||||
(UInt32) strlen(utf8ServiceName), utf8ServiceName,
|
||||
(UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
|
||||
(UInt32) strlen(utf8Password), utf8Password,
|
||||
dontWantItemRef);
|
||||
BOOL didSucceed = (err == noErr);
|
||||
if (didSucceed) {
|
||||
// write to preferences that we have a keychain item (so we know later
|
||||
// that we can read from the keychain without raising a permissions dialog)
|
||||
NSString *prefKey = [self prefsKeyForName:keychainItemName];
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
[defaults setBool:YES forKey:prefKey];
|
||||
}
|
||||
|
||||
return didSucceed;
|
||||
}
|
||||
|
||||
+ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName {
|
||||
|
||||
SecKeychainRef defaultKeychain = NULL;
|
||||
SecKeychainItemRef itemRef = NULL;
|
||||
const char *utf8ServiceName = [keychainItemName UTF8String];
|
||||
|
||||
// we don't really care about the password here, we just want to
|
||||
// get the SecKeychainItemRef so we can delete it.
|
||||
OSStatus err = SecKeychainFindGenericPassword (defaultKeychain,
|
||||
(UInt32) strlen(utf8ServiceName), utf8ServiceName,
|
||||
(UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
|
||||
0, NULL, // ignore password
|
||||
&itemRef);
|
||||
if (err != noErr) {
|
||||
// failure to find is success
|
||||
return YES;
|
||||
} else {
|
||||
// found something, so delete it
|
||||
err = SecKeychainItemDelete(itemRef);
|
||||
CFRelease(itemRef);
|
||||
|
||||
// remove our preference key
|
||||
NSString *prefKey = [self prefsKeyForName:keychainItemName];
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
[defaults removeObjectForKey:prefKey];
|
||||
|
||||
return (err == noErr);
|
||||
}
|
||||
}
|
||||
|
||||
#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
|
||||
+ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
|
||||
clientID:(NSString *)clientID
|
||||
clientSecret:(NSString *)clientSecret {
|
||||
Class signInClass = [self signInClass];
|
||||
NSURL *tokenURL = [signInClass googleTokenURL];
|
||||
NSString *redirectURI = [signInClass nativeClientRedirectURI];
|
||||
|
||||
GTMOAuth2Authentication *auth;
|
||||
auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
|
||||
tokenURL:tokenURL
|
||||
redirectURI:redirectURI
|
||||
clientID:clientID
|
||||
clientSecret:clientSecret];
|
||||
|
||||
[GTMOAuth2WindowController authorizeFromKeychainForName:keychainItemName
|
||||
authentication:auth];
|
||||
return auth;
|
||||
}
|
||||
#endif
|
||||
|
||||
+ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName
|
||||
authentication:(GTMOAuth2Authentication *)newAuth {
|
||||
[newAuth setAccessToken:nil];
|
||||
|
||||
// before accessing the keychain, check preferences to verify that we've
|
||||
// previously saved a token to the keychain (so we don't needlessly raise
|
||||
// a keychain access permission dialog)
|
||||
NSString *prefKey = [self prefsKeyForName:keychainItemName];
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
BOOL flag = [defaults boolForKey:prefKey];
|
||||
if (!flag) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
BOOL didGetTokens = NO;
|
||||
|
||||
SecKeychainRef defaultKeychain = NULL;
|
||||
const char *utf8ServiceName = [keychainItemName UTF8String];
|
||||
SecKeychainItemRef *dontWantItemRef = NULL;
|
||||
|
||||
void *passwordBuff = NULL;
|
||||
UInt32 passwordBuffLength = 0;
|
||||
|
||||
OSStatus err = SecKeychainFindGenericPassword(defaultKeychain,
|
||||
(UInt32) strlen(utf8ServiceName), utf8ServiceName,
|
||||
(UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
|
||||
&passwordBuffLength, &passwordBuff,
|
||||
dontWantItemRef);
|
||||
if (err == noErr && passwordBuff != NULL) {
|
||||
|
||||
NSString *password = [[[NSString alloc] initWithBytes:passwordBuff
|
||||
length:passwordBuffLength
|
||||
encoding:NSUTF8StringEncoding] autorelease];
|
||||
|
||||
// free the password buffer that was allocated above
|
||||
SecKeychainItemFreeContent(NULL, passwordBuff);
|
||||
|
||||
if (password != nil) {
|
||||
[newAuth setKeysForResponseString:password];
|
||||
didGetTokens = YES;
|
||||
}
|
||||
}
|
||||
return didGetTokens;
|
||||
}
|
||||
|
||||
#pragma mark User Properties
|
||||
|
||||
- (void)setProperty:(id)obj forKey:(NSString *)key {
|
||||
if (obj == nil) {
|
||||
// User passed in nil, so delete the property
|
||||
[properties_ removeObjectForKey:key];
|
||||
} else {
|
||||
// Be sure the property dictionary exists
|
||||
if (properties_ == nil) {
|
||||
[self setProperties:[NSMutableDictionary dictionary]];
|
||||
}
|
||||
[properties_ setObject:obj forKey:key];
|
||||
}
|
||||
}
|
||||
|
||||
- (id)propertyForKey:(NSString *)key {
|
||||
id obj = [properties_ objectForKey:key];
|
||||
|
||||
// Be sure the returned pointer has the life of the autorelease pool,
|
||||
// in case self is released immediately
|
||||
return [[obj retain] autorelease];
|
||||
}
|
||||
|
||||
#pragma mark Accessors
|
||||
|
||||
- (GTMOAuth2Authentication *)authentication {
|
||||
return self.signIn.authentication;
|
||||
}
|
||||
|
||||
- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val {
|
||||
self.signIn.networkLossTimeoutInterval = val;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)networkLossTimeoutInterval {
|
||||
return self.signIn.networkLossTimeoutInterval;
|
||||
}
|
||||
|
||||
- (BOOL)shouldUseKeychain {
|
||||
NSString *name = self.keychainItemName;
|
||||
return ([name length] > 0);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif // #if !TARGET_OS_IPHONE
|
||||
|
||||
#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
|
||||
13
OAuth 2/OAuth2.h
Executable file
13
OAuth 2/OAuth2.h
Executable file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// NSObject_OAuth2.h
|
||||
// Notifications for YouTube
|
||||
//
|
||||
// Created by Kim Wittenburg on 22.09.12.
|
||||
// Copyright (c) 2012 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "GTMHTTPFetcher.h"
|
||||
#import "GTMHTTPFetchHistory.h"
|
||||
#import "GTMOAuth2Authentication.h"
|
||||
#import "GTMOAuth2SignIn.h"
|
||||
#import "GTMOAuth2WindowController.h"
|
||||
568
OAuth 2/de.lproj/GTMOAuth2Window.xib
Executable file
568
OAuth 2/de.lproj/GTMOAuth2Window.xib
Executable file
@@ -0,0 +1,568 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
|
||||
<data>
|
||||
<int key="IBDocument.SystemTarget">1050</int>
|
||||
<string key="IBDocument.SystemVersion">12E55</string>
|
||||
<string key="IBDocument.InterfaceBuilderVersion">3084</string>
|
||||
<string key="IBDocument.AppKitVersion">1187.39</string>
|
||||
<string key="IBDocument.HIToolboxVersion">626.00</string>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.WebKitIBPlugin</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>3084</string>
|
||||
<string>2053</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSArray" key="IBDocument.IntegratedClassDependencies">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>NSButton</string>
|
||||
<string>NSButtonCell</string>
|
||||
<string>NSCustomObject</string>
|
||||
<string>NSUserDefaultsController</string>
|
||||
<string>NSView</string>
|
||||
<string>NSWindowTemplate</string>
|
||||
<string>WebView</string>
|
||||
</object>
|
||||
<object class="NSArray" key="IBDocument.PluginDependencies">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.WebKitIBPlugin</string>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="IBDocument.Metadata">
|
||||
<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
|
||||
<integer value="1" key="NS.object.0"/>
|
||||
</object>
|
||||
<object class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSCustomObject" id="1001">
|
||||
<string key="NSClassName">GTMOAuth2WindowController</string>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="1003">
|
||||
<string key="NSClassName">FirstResponder</string>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="1004">
|
||||
<string key="NSClassName">NSApplication</string>
|
||||
</object>
|
||||
<object class="NSWindowTemplate" id="996099970">
|
||||
<int key="NSWindowStyleMask">11</int>
|
||||
<int key="NSWindowBacking">2</int>
|
||||
<string key="NSWindowRect">{{522, 328}, {515, 419}}</string>
|
||||
<int key="NSWTFlags">536870912</int>
|
||||
<string key="NSWindowTitle">Anmelden</string>
|
||||
<string key="NSWindowClass">NSWindow</string>
|
||||
<nil key="NSViewClass"/>
|
||||
<nil key="NSUserInterfaceItemIdentifier"/>
|
||||
<string key="NSWindowContentMinSize">{475, 290}</string>
|
||||
<object class="NSView" key="NSWindowView" id="563959409">
|
||||
<reference key="NSNextResponder"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="WebView" id="697605106">
|
||||
<reference key="NSNextResponder" ref="563959409"/>
|
||||
<int key="NSvFlags">274</int>
|
||||
<object class="NSMutableSet" key="NSDragTypes">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="set.sortedObjects">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>Apple HTML pasteboard type</string>
|
||||
<string>Apple PDF pasteboard type</string>
|
||||
<string>Apple PICT pasteboard type</string>
|
||||
<string>Apple URL pasteboard type</string>
|
||||
<string>Apple Web Archive pasteboard type</string>
|
||||
<string>NSColor pasteboard type</string>
|
||||
<string>NSFilenamesPboardType</string>
|
||||
<string>NSStringPboardType</string>
|
||||
<string>NeXT RTFD pasteboard type</string>
|
||||
<string>NeXT Rich Text Format v1.0 pasteboard type</string>
|
||||
<string>NeXT TIFF v4.0 pasteboard type</string>
|
||||
<string>WebURLsWithTitlesPboardType</string>
|
||||
<string>public.png</string>
|
||||
<string>public.url</string>
|
||||
<string>public.url-name</string>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{0, 20}, {515, 399}}</string>
|
||||
<reference key="NSSuperview" ref="563959409"/>
|
||||
<reference key="NSNextKeyView"/>
|
||||
<string key="FrameName"/>
|
||||
<string key="GroupName"/>
|
||||
<object class="WebPreferences" key="Preferences">
|
||||
<string key="Identifier"/>
|
||||
<object class="NSMutableDictionary" key="Values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>WebKitDefaultFixedFontSize</string>
|
||||
<string>WebKitDefaultFontSize</string>
|
||||
<string>WebKitMinimumFontSize</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<integer value="12"/>
|
||||
<integer value="12"/>
|
||||
<integer value="1"/>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<bool key="UseBackForwardList">YES</bool>
|
||||
<bool key="AllowsUndo">YES</bool>
|
||||
</object>
|
||||
<object class="NSButton" id="736908656">
|
||||
<reference key="NSNextResponder" ref="563959409"/>
|
||||
<int key="NSvFlags">289</int>
|
||||
<string key="NSFrame">{{479, 0}, {16, 19}}</string>
|
||||
<reference key="NSSuperview" ref="563959409"/>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="215388286">
|
||||
<int key="NSCellFlags">67108864</int>
|
||||
<int key="NSCellFlags2">134217728</int>
|
||||
<string key="NSContents"/>
|
||||
<object class="NSFont" key="NSSupport" id="895134484">
|
||||
<string key="NSName">LucidaGrande</string>
|
||||
<double key="NSSize">13</double>
|
||||
<int key="NSfFlags">1044</int>
|
||||
</object>
|
||||
<reference key="NSControlView" ref="736908656"/>
|
||||
<int key="NSButtonFlags">-2041823232</int>
|
||||
<int key="NSButtonFlags2">134</int>
|
||||
<object class="NSCustomResource" key="NSNormalImage">
|
||||
<string key="NSClassName">NSImage</string>
|
||||
<string key="NSResourceName">NSStopProgressTemplate</string>
|
||||
</object>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string type="base64-UTF8" key="NSKeyEquivalent">Gw</string>
|
||||
<int key="NSPeriodicDelay">400</int>
|
||||
<int key="NSPeriodicInterval">75</int>
|
||||
</object>
|
||||
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
|
||||
</object>
|
||||
<object class="NSButton" id="771759786">
|
||||
<reference key="NSNextResponder" ref="563959409"/>
|
||||
<int key="NSvFlags">289</int>
|
||||
<string key="NSFrame">{{437, 0}, {16, 19}}</string>
|
||||
<reference key="NSSuperview" ref="563959409"/>
|
||||
<reference key="NSNextKeyView" ref="36322049"/>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="656055052">
|
||||
<int key="NSCellFlags">67108864</int>
|
||||
<int key="NSCellFlags2">134217728</int>
|
||||
<string key="NSContents"/>
|
||||
<reference key="NSSupport" ref="895134484"/>
|
||||
<reference key="NSControlView" ref="771759786"/>
|
||||
<int key="NSButtonFlags">-2041823232</int>
|
||||
<int key="NSButtonFlags2">134</int>
|
||||
<object class="NSCustomResource" key="NSNormalImage">
|
||||
<string key="NSClassName">NSImage</string>
|
||||
<string key="NSResourceName">NSGoLeftTemplate</string>
|
||||
</object>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string key="NSKeyEquivalent"/>
|
||||
<int key="NSPeriodicDelay">400</int>
|
||||
<int key="NSPeriodicInterval">75</int>
|
||||
</object>
|
||||
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
|
||||
</object>
|
||||
<object class="NSButton" id="36322049">
|
||||
<reference key="NSNextResponder" ref="563959409"/>
|
||||
<int key="NSvFlags">289</int>
|
||||
<string key="NSFrame">{{456, 0}, {16, 19}}</string>
|
||||
<reference key="NSSuperview" ref="563959409"/>
|
||||
<reference key="NSNextKeyView" ref="736908656"/>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="282674264">
|
||||
<int key="NSCellFlags">67108864</int>
|
||||
<int key="NSCellFlags2">134217728</int>
|
||||
<string key="NSContents"/>
|
||||
<reference key="NSSupport" ref="895134484"/>
|
||||
<reference key="NSControlView" ref="36322049"/>
|
||||
<int key="NSButtonFlags">-2042347520</int>
|
||||
<int key="NSButtonFlags2">134</int>
|
||||
<object class="NSCustomResource" key="NSNormalImage">
|
||||
<string key="NSClassName">NSImage</string>
|
||||
<string key="NSResourceName">NSGoRightTemplate</string>
|
||||
</object>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string key="NSKeyEquivalent"/>
|
||||
<int key="NSPeriodicDelay">400</int>
|
||||
<int key="NSPeriodicInterval">75</int>
|
||||
</object>
|
||||
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
|
||||
</object>
|
||||
<object class="NSButton" id="823054555">
|
||||
<reference key="NSNextResponder" ref="563959409"/>
|
||||
<int key="NSvFlags">292</int>
|
||||
<string key="NSFrame">{{2, 1}, {429, 18}}</string>
|
||||
<reference key="NSSuperview" ref="563959409"/>
|
||||
<reference key="NSNextKeyView" ref="771759786"/>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="817997953">
|
||||
<int key="NSCellFlags">-2080374784</int>
|
||||
<int key="NSCellFlags2">131072</int>
|
||||
<string key="NSContents">Kennwort in meinem Schlüsselbund sichern</string>
|
||||
<object class="NSFont" key="NSSupport">
|
||||
<string key="NSName">LucidaGrande</string>
|
||||
<double key="NSSize">11</double>
|
||||
<int key="NSfFlags">3100</int>
|
||||
</object>
|
||||
<reference key="NSControlView" ref="823054555"/>
|
||||
<int key="NSButtonFlags">1211912448</int>
|
||||
<int key="NSButtonFlags2">2</int>
|
||||
<object class="NSCustomResource" key="NSNormalImage">
|
||||
<string key="NSClassName">NSImage</string>
|
||||
<string key="NSResourceName">NSSwitch</string>
|
||||
</object>
|
||||
<object class="NSButtonImageSource" key="NSAlternateImage">
|
||||
<string key="NSImageName">NSSwitch</string>
|
||||
</object>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string key="NSKeyEquivalent"/>
|
||||
<int key="NSPeriodicDelay">200</int>
|
||||
<int key="NSPeriodicInterval">25</int>
|
||||
</object>
|
||||
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrameSize">{515, 419}</string>
|
||||
<reference key="NSSuperview"/>
|
||||
<reference key="NSNextKeyView" ref="697605106"/>
|
||||
</object>
|
||||
<string key="NSScreenRect">{{0, 0}, {1680, 1028}}</string>
|
||||
<string key="NSMinSize">{475, 312}</string>
|
||||
<string key="NSMaxSize">{10000000000000, 10000000000000}</string>
|
||||
<bool key="NSWindowIsRestorable">YES</bool>
|
||||
</object>
|
||||
<object class="NSUserDefaultsController" id="681499023">
|
||||
<bool key="NSSharedInstance">YES</bool>
|
||||
</object>
|
||||
</object>
|
||||
<object class="IBObjectContainer" key="IBDocument.Objects">
|
||||
<object class="NSMutableArray" key="connectionRecords">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">window</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="996099970"/>
|
||||
</object>
|
||||
<int key="connectionID">8</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">closeWindow:</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="736908656"/>
|
||||
</object>
|
||||
<int key="connectionID">42</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">keychainCheckbox</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="823054555"/>
|
||||
</object>
|
||||
<int key="connectionID">46</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">webBackButton</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="771759786"/>
|
||||
</object>
|
||||
<int key="connectionID">47</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">webCloseButton</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="736908656"/>
|
||||
</object>
|
||||
<int key="connectionID">48</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">webView</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="697605106"/>
|
||||
</object>
|
||||
<int key="connectionID">49</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">toggleStorePasswordInKeychain:</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="823054555"/>
|
||||
</object>
|
||||
<int key="connectionID">50</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">delegate</string>
|
||||
<reference key="source" ref="996099970"/>
|
||||
<reference key="destination" ref="1001"/>
|
||||
</object>
|
||||
<int key="connectionID">7</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">goBack:</string>
|
||||
<reference key="source" ref="697605106"/>
|
||||
<reference key="destination" ref="771759786"/>
|
||||
</object>
|
||||
<int key="connectionID">28</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">goForward:</string>
|
||||
<reference key="source" ref="697605106"/>
|
||||
<reference key="destination" ref="36322049"/>
|
||||
</object>
|
||||
<int key="connectionID">29</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBBindingConnection" key="connection">
|
||||
<string key="label">enabled: webView.canGoBack</string>
|
||||
<reference key="source" ref="771759786"/>
|
||||
<reference key="destination" ref="1001"/>
|
||||
<object class="NSNibBindingConnector" key="connector">
|
||||
<reference key="NSSource" ref="771759786"/>
|
||||
<reference key="NSDestination" ref="1001"/>
|
||||
<string key="NSLabel">enabled: webView.canGoBack</string>
|
||||
<string key="NSBinding">enabled</string>
|
||||
<string key="NSKeyPath">webView.canGoBack</string>
|
||||
<int key="NSNibBindingConnectorVersion">2</int>
|
||||
</object>
|
||||
</object>
|
||||
<int key="connectionID">31</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBBindingConnection" key="connection">
|
||||
<string key="label">enabled: webView.canGoForward</string>
|
||||
<reference key="source" ref="36322049"/>
|
||||
<reference key="destination" ref="1001"/>
|
||||
<object class="NSNibBindingConnector" key="connector">
|
||||
<reference key="NSSource" ref="36322049"/>
|
||||
<reference key="NSDestination" ref="1001"/>
|
||||
<string key="NSLabel">enabled: webView.canGoForward</string>
|
||||
<string key="NSBinding">enabled</string>
|
||||
<string key="NSKeyPath">webView.canGoForward</string>
|
||||
<int key="NSNibBindingConnectorVersion">2</int>
|
||||
</object>
|
||||
</object>
|
||||
<int key="connectionID">35</int>
|
||||
</object>
|
||||
</object>
|
||||
<object class="IBMutableOrderedSet" key="objectRecords">
|
||||
<object class="NSArray" key="orderedObjects">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">0</int>
|
||||
<object class="NSArray" key="object" id="0">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
</object>
|
||||
<reference key="children" ref="1000"/>
|
||||
<nil key="parent"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-2</int>
|
||||
<reference key="object" ref="1001"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">File's Owner</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-1</int>
|
||||
<reference key="object" ref="1003"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">First Responder</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-3</int>
|
||||
<reference key="object" ref="1004"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">Application</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">3</int>
|
||||
<reference key="object" ref="996099970"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="563959409"/>
|
||||
</object>
|
||||
<reference key="parent" ref="0"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">4</int>
|
||||
<reference key="object" ref="563959409"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="697605106"/>
|
||||
<reference ref="823054555"/>
|
||||
<reference ref="36322049"/>
|
||||
<reference ref="771759786"/>
|
||||
<reference ref="736908656"/>
|
||||
</object>
|
||||
<reference key="parent" ref="996099970"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">5</int>
|
||||
<reference key="object" ref="697605106"/>
|
||||
<reference key="parent" ref="563959409"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">17</int>
|
||||
<reference key="object" ref="736908656"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="215388286"/>
|
||||
</object>
|
||||
<reference key="parent" ref="563959409"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">18</int>
|
||||
<reference key="object" ref="215388286"/>
|
||||
<reference key="parent" ref="736908656"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">19</int>
|
||||
<reference key="object" ref="771759786"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="656055052"/>
|
||||
</object>
|
||||
<reference key="parent" ref="563959409"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">20</int>
|
||||
<reference key="object" ref="656055052"/>
|
||||
<reference key="parent" ref="771759786"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">26</int>
|
||||
<reference key="object" ref="36322049"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="282674264"/>
|
||||
</object>
|
||||
<reference key="parent" ref="563959409"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">27</int>
|
||||
<reference key="object" ref="282674264"/>
|
||||
<reference key="parent" ref="36322049"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">32</int>
|
||||
<reference key="object" ref="681499023"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">43</int>
|
||||
<reference key="object" ref="823054555"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="817997953"/>
|
||||
</object>
|
||||
<reference key="parent" ref="563959409"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">44</int>
|
||||
<reference key="object" ref="817997953"/>
|
||||
<reference key="parent" ref="823054555"/>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="flattenedProperties">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>-1.IBPluginDependency</string>
|
||||
<string>-2.IBPluginDependency</string>
|
||||
<string>-3.IBPluginDependency</string>
|
||||
<string>17.IBPluginDependency</string>
|
||||
<string>18.IBPluginDependency</string>
|
||||
<string>19.IBPluginDependency</string>
|
||||
<string>20.IBPluginDependency</string>
|
||||
<string>26.IBPluginDependency</string>
|
||||
<string>27.IBPluginDependency</string>
|
||||
<string>3.IBPluginDependency</string>
|
||||
<string>3.IBWindowTemplateEditedContentRect</string>
|
||||
<string>3.NSWindowTemplate.visibleAtLaunch</string>
|
||||
<string>32.IBPluginDependency</string>
|
||||
<string>4.IBPluginDependency</string>
|
||||
<string>43.IBPluginDependency</string>
|
||||
<string>44.IBPluginDependency</string>
|
||||
<string>5.IBPluginDependency</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>{{112, 709}, {515, 419}}</string>
|
||||
<boolean value="NO"/>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.WebKitIBPlugin</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="unlocalizedProperties">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference key="dict.sortedKeys" ref="0"/>
|
||||
<reference key="dict.values" ref="0"/>
|
||||
</object>
|
||||
<nil key="activeLocalization"/>
|
||||
<object class="NSMutableDictionary" key="localizations">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference key="dict.sortedKeys" ref="0"/>
|
||||
<reference key="dict.values" ref="0"/>
|
||||
</object>
|
||||
<nil key="sourceID"/>
|
||||
<int key="maxID">50</int>
|
||||
</object>
|
||||
<object class="IBClassDescriber" key="IBDocument.Classes"/>
|
||||
<int key="IBDocument.localizationMode">0</int>
|
||||
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
|
||||
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
|
||||
<integer value="1050" key="NS.object.0"/>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
|
||||
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string>
|
||||
<integer value="3000" key="NS.object.0"/>
|
||||
</object>
|
||||
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
|
||||
<int key="IBDocument.defaultPropertyAccessControl">3</int>
|
||||
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>NSGoLeftTemplate</string>
|
||||
<string>NSGoRightTemplate</string>
|
||||
<string>NSStopProgressTemplate</string>
|
||||
<string>NSSwitch</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>{9, 9}</string>
|
||||
<string>{9, 9}</string>
|
||||
<string>{11, 11}</string>
|
||||
<string>{15, 15}</string>
|
||||
</object>
|
||||
</object>
|
||||
</data>
|
||||
</archive>
|
||||
664
OAuth 2/en.lproj/GTMOAuth2Window.xib
Executable file
664
OAuth 2/en.lproj/GTMOAuth2Window.xib
Executable file
@@ -0,0 +1,664 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
|
||||
<data>
|
||||
<int key="IBDocument.SystemTarget">1050</int>
|
||||
<string key="IBDocument.SystemVersion">12E55</string>
|
||||
<string key="IBDocument.InterfaceBuilderVersion">3084</string>
|
||||
<string key="IBDocument.AppKitVersion">1187.39</string>
|
||||
<string key="IBDocument.HIToolboxVersion">626.00</string>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.WebKitIBPlugin</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>3084</string>
|
||||
<string>2053</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSArray" key="IBDocument.IntegratedClassDependencies">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>NSButton</string>
|
||||
<string>NSButtonCell</string>
|
||||
<string>NSCustomObject</string>
|
||||
<string>NSUserDefaultsController</string>
|
||||
<string>NSView</string>
|
||||
<string>NSWindowTemplate</string>
|
||||
<string>WebView</string>
|
||||
</object>
|
||||
<object class="NSArray" key="IBDocument.PluginDependencies">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.WebKitIBPlugin</string>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="IBDocument.Metadata">
|
||||
<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
|
||||
<integer value="1" key="NS.object.0"/>
|
||||
</object>
|
||||
<object class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSCustomObject" id="1001">
|
||||
<string key="NSClassName">GTMOAuth2WindowController</string>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="1003">
|
||||
<string key="NSClassName">FirstResponder</string>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="1004">
|
||||
<string key="NSClassName">NSApplication</string>
|
||||
</object>
|
||||
<object class="NSWindowTemplate" id="996099970">
|
||||
<int key="NSWindowStyleMask">11</int>
|
||||
<int key="NSWindowBacking">2</int>
|
||||
<string key="NSWindowRect">{{558, 328}, {515, 419}}</string>
|
||||
<int key="NSWTFlags">536870912</int>
|
||||
<string key="NSWindowTitle">Sign In</string>
|
||||
<string key="NSWindowClass">NSWindow</string>
|
||||
<nil key="NSViewClass"/>
|
||||
<nil key="NSUserInterfaceItemIdentifier"/>
|
||||
<string key="NSWindowContentMinSize">{475, 290}</string>
|
||||
<object class="NSView" key="NSWindowView" id="563959409">
|
||||
<reference key="NSNextResponder"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="WebView" id="697605106">
|
||||
<reference key="NSNextResponder" ref="563959409"/>
|
||||
<int key="NSvFlags">274</int>
|
||||
<object class="NSMutableSet" key="NSDragTypes">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="set.sortedObjects">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>Apple HTML pasteboard type</string>
|
||||
<string>Apple PDF pasteboard type</string>
|
||||
<string>Apple PICT pasteboard type</string>
|
||||
<string>Apple URL pasteboard type</string>
|
||||
<string>Apple Web Archive pasteboard type</string>
|
||||
<string>NSColor pasteboard type</string>
|
||||
<string>NSFilenamesPboardType</string>
|
||||
<string>NSStringPboardType</string>
|
||||
<string>NeXT RTFD pasteboard type</string>
|
||||
<string>NeXT Rich Text Format v1.0 pasteboard type</string>
|
||||
<string>NeXT TIFF v4.0 pasteboard type</string>
|
||||
<string>WebURLsWithTitlesPboardType</string>
|
||||
<string>public.png</string>
|
||||
<string>public.url</string>
|
||||
<string>public.url-name</string>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{0, 20}, {515, 399}}</string>
|
||||
<reference key="NSSuperview" ref="563959409"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView"/>
|
||||
<string key="FrameName"/>
|
||||
<string key="GroupName"/>
|
||||
<object class="WebPreferences" key="Preferences">
|
||||
<string key="Identifier"/>
|
||||
<object class="NSMutableDictionary" key="Values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>WebKitDefaultFixedFontSize</string>
|
||||
<string>WebKitDefaultFontSize</string>
|
||||
<string>WebKitMinimumFontSize</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<integer value="12"/>
|
||||
<integer value="12"/>
|
||||
<integer value="1"/>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<bool key="UseBackForwardList">YES</bool>
|
||||
<bool key="AllowsUndo">YES</bool>
|
||||
</object>
|
||||
<object class="NSButton" id="736908656">
|
||||
<reference key="NSNextResponder" ref="563959409"/>
|
||||
<int key="NSvFlags">289</int>
|
||||
<string key="NSFrame">{{479, 0}, {16, 19}}</string>
|
||||
<reference key="NSSuperview" ref="563959409"/>
|
||||
<reference key="NSWindow"/>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="215388286">
|
||||
<int key="NSCellFlags">67108864</int>
|
||||
<int key="NSCellFlags2">134217728</int>
|
||||
<string key="NSContents"/>
|
||||
<object class="NSFont" key="NSSupport" id="895134484">
|
||||
<string key="NSName">LucidaGrande</string>
|
||||
<double key="NSSize">13</double>
|
||||
<int key="NSfFlags">1044</int>
|
||||
</object>
|
||||
<reference key="NSControlView" ref="736908656"/>
|
||||
<int key="NSButtonFlags">-2041823232</int>
|
||||
<int key="NSButtonFlags2">134</int>
|
||||
<object class="NSCustomResource" key="NSNormalImage">
|
||||
<string key="NSClassName">NSImage</string>
|
||||
<string key="NSResourceName">NSStopProgressTemplate</string>
|
||||
</object>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string type="base64-UTF8" key="NSKeyEquivalent">Gw</string>
|
||||
<int key="NSPeriodicDelay">400</int>
|
||||
<int key="NSPeriodicInterval">75</int>
|
||||
</object>
|
||||
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
|
||||
</object>
|
||||
<object class="NSButton" id="771759786">
|
||||
<reference key="NSNextResponder" ref="563959409"/>
|
||||
<int key="NSvFlags">289</int>
|
||||
<string key="NSFrame">{{437, 0}, {16, 19}}</string>
|
||||
<reference key="NSSuperview" ref="563959409"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="36322049"/>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="656055052">
|
||||
<int key="NSCellFlags">67108864</int>
|
||||
<int key="NSCellFlags2">134217728</int>
|
||||
<string key="NSContents"/>
|
||||
<reference key="NSSupport" ref="895134484"/>
|
||||
<reference key="NSControlView" ref="771759786"/>
|
||||
<int key="NSButtonFlags">-2041823232</int>
|
||||
<int key="NSButtonFlags2">134</int>
|
||||
<object class="NSCustomResource" key="NSNormalImage">
|
||||
<string key="NSClassName">NSImage</string>
|
||||
<string key="NSResourceName">NSGoLeftTemplate</string>
|
||||
</object>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string key="NSKeyEquivalent"/>
|
||||
<int key="NSPeriodicDelay">400</int>
|
||||
<int key="NSPeriodicInterval">75</int>
|
||||
</object>
|
||||
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
|
||||
</object>
|
||||
<object class="NSButton" id="36322049">
|
||||
<reference key="NSNextResponder" ref="563959409"/>
|
||||
<int key="NSvFlags">289</int>
|
||||
<string key="NSFrame">{{456, 0}, {16, 19}}</string>
|
||||
<reference key="NSSuperview" ref="563959409"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="736908656"/>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="282674264">
|
||||
<int key="NSCellFlags">67108864</int>
|
||||
<int key="NSCellFlags2">134217728</int>
|
||||
<string key="NSContents"/>
|
||||
<reference key="NSSupport" ref="895134484"/>
|
||||
<reference key="NSControlView" ref="36322049"/>
|
||||
<int key="NSButtonFlags">-2042347520</int>
|
||||
<int key="NSButtonFlags2">134</int>
|
||||
<object class="NSCustomResource" key="NSNormalImage">
|
||||
<string key="NSClassName">NSImage</string>
|
||||
<string key="NSResourceName">NSGoRightTemplate</string>
|
||||
</object>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string key="NSKeyEquivalent"/>
|
||||
<int key="NSPeriodicDelay">400</int>
|
||||
<int key="NSPeriodicInterval">75</int>
|
||||
</object>
|
||||
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
|
||||
</object>
|
||||
<object class="NSButton" id="823054555">
|
||||
<reference key="NSNextResponder" ref="563959409"/>
|
||||
<int key="NSvFlags">292</int>
|
||||
<string key="NSFrame">{{2, 1}, {429, 18}}</string>
|
||||
<reference key="NSSuperview" ref="563959409"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="771759786"/>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="817997953">
|
||||
<int key="NSCellFlags">-2080374784</int>
|
||||
<int key="NSCellFlags2">131072</int>
|
||||
<string key="NSContents">Save password in my keychain</string>
|
||||
<object class="NSFont" key="NSSupport">
|
||||
<string key="NSName">LucidaGrande</string>
|
||||
<double key="NSSize">11</double>
|
||||
<int key="NSfFlags">3100</int>
|
||||
</object>
|
||||
<reference key="NSControlView" ref="823054555"/>
|
||||
<int key="NSButtonFlags">1211912448</int>
|
||||
<int key="NSButtonFlags2">2</int>
|
||||
<object class="NSCustomResource" key="NSNormalImage">
|
||||
<string key="NSClassName">NSImage</string>
|
||||
<string key="NSResourceName">NSSwitch</string>
|
||||
</object>
|
||||
<object class="NSButtonImageSource" key="NSAlternateImage">
|
||||
<string key="NSImageName">NSSwitch</string>
|
||||
</object>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string key="NSKeyEquivalent"/>
|
||||
<int key="NSPeriodicDelay">200</int>
|
||||
<int key="NSPeriodicInterval">25</int>
|
||||
</object>
|
||||
<bool key="NSAllowsLogicalLayoutDirection">NO</bool>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrameSize">{515, 419}</string>
|
||||
<reference key="NSSuperview"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="697605106"/>
|
||||
</object>
|
||||
<string key="NSScreenRect">{{0, 0}, {1680, 1028}}</string>
|
||||
<string key="NSMinSize">{475, 312}</string>
|
||||
<string key="NSMaxSize">{10000000000000, 10000000000000}</string>
|
||||
<bool key="NSWindowIsRestorable">YES</bool>
|
||||
</object>
|
||||
<object class="NSUserDefaultsController" id="681499023">
|
||||
<bool key="NSSharedInstance">YES</bool>
|
||||
</object>
|
||||
</object>
|
||||
<object class="IBObjectContainer" key="IBDocument.Objects">
|
||||
<object class="NSMutableArray" key="connectionRecords">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">window</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="996099970"/>
|
||||
</object>
|
||||
<int key="connectionID">8</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">closeWindow:</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="736908656"/>
|
||||
</object>
|
||||
<int key="connectionID">42</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">keychainCheckbox</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="823054555"/>
|
||||
</object>
|
||||
<int key="connectionID">46</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">webBackButton</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="771759786"/>
|
||||
</object>
|
||||
<int key="connectionID">47</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">webCloseButton</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="736908656"/>
|
||||
</object>
|
||||
<int key="connectionID">48</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">webView</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="697605106"/>
|
||||
</object>
|
||||
<int key="connectionID">49</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">toggleStorePasswordInKeychain:</string>
|
||||
<reference key="source" ref="1001"/>
|
||||
<reference key="destination" ref="823054555"/>
|
||||
</object>
|
||||
<int key="connectionID">50</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">delegate</string>
|
||||
<reference key="source" ref="996099970"/>
|
||||
<reference key="destination" ref="1001"/>
|
||||
</object>
|
||||
<int key="connectionID">7</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">goBack:</string>
|
||||
<reference key="source" ref="697605106"/>
|
||||
<reference key="destination" ref="771759786"/>
|
||||
</object>
|
||||
<int key="connectionID">28</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">goForward:</string>
|
||||
<reference key="source" ref="697605106"/>
|
||||
<reference key="destination" ref="36322049"/>
|
||||
</object>
|
||||
<int key="connectionID">29</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBBindingConnection" key="connection">
|
||||
<string key="label">enabled: webView.canGoBack</string>
|
||||
<reference key="source" ref="771759786"/>
|
||||
<reference key="destination" ref="1001"/>
|
||||
<object class="NSNibBindingConnector" key="connector">
|
||||
<reference key="NSSource" ref="771759786"/>
|
||||
<reference key="NSDestination" ref="1001"/>
|
||||
<string key="NSLabel">enabled: webView.canGoBack</string>
|
||||
<string key="NSBinding">enabled</string>
|
||||
<string key="NSKeyPath">webView.canGoBack</string>
|
||||
<int key="NSNibBindingConnectorVersion">2</int>
|
||||
</object>
|
||||
</object>
|
||||
<int key="connectionID">31</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBBindingConnection" key="connection">
|
||||
<string key="label">enabled: webView.canGoForward</string>
|
||||
<reference key="source" ref="36322049"/>
|
||||
<reference key="destination" ref="1001"/>
|
||||
<object class="NSNibBindingConnector" key="connector">
|
||||
<reference key="NSSource" ref="36322049"/>
|
||||
<reference key="NSDestination" ref="1001"/>
|
||||
<string key="NSLabel">enabled: webView.canGoForward</string>
|
||||
<string key="NSBinding">enabled</string>
|
||||
<string key="NSKeyPath">webView.canGoForward</string>
|
||||
<int key="NSNibBindingConnectorVersion">2</int>
|
||||
</object>
|
||||
</object>
|
||||
<int key="connectionID">35</int>
|
||||
</object>
|
||||
</object>
|
||||
<object class="IBMutableOrderedSet" key="objectRecords">
|
||||
<object class="NSArray" key="orderedObjects">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">0</int>
|
||||
<object class="NSArray" key="object" id="0">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
</object>
|
||||
<reference key="children" ref="1000"/>
|
||||
<nil key="parent"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-2</int>
|
||||
<reference key="object" ref="1001"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">File's Owner</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-1</int>
|
||||
<reference key="object" ref="1003"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">First Responder</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-3</int>
|
||||
<reference key="object" ref="1004"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">Application</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">3</int>
|
||||
<reference key="object" ref="996099970"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="563959409"/>
|
||||
</object>
|
||||
<reference key="parent" ref="0"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">4</int>
|
||||
<reference key="object" ref="563959409"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="697605106"/>
|
||||
<reference ref="823054555"/>
|
||||
<reference ref="36322049"/>
|
||||
<reference ref="771759786"/>
|
||||
<reference ref="736908656"/>
|
||||
</object>
|
||||
<reference key="parent" ref="996099970"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">5</int>
|
||||
<reference key="object" ref="697605106"/>
|
||||
<reference key="parent" ref="563959409"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">17</int>
|
||||
<reference key="object" ref="736908656"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="215388286"/>
|
||||
</object>
|
||||
<reference key="parent" ref="563959409"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">18</int>
|
||||
<reference key="object" ref="215388286"/>
|
||||
<reference key="parent" ref="736908656"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">19</int>
|
||||
<reference key="object" ref="771759786"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="656055052"/>
|
||||
</object>
|
||||
<reference key="parent" ref="563959409"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">20</int>
|
||||
<reference key="object" ref="656055052"/>
|
||||
<reference key="parent" ref="771759786"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">26</int>
|
||||
<reference key="object" ref="36322049"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="282674264"/>
|
||||
</object>
|
||||
<reference key="parent" ref="563959409"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">27</int>
|
||||
<reference key="object" ref="282674264"/>
|
||||
<reference key="parent" ref="36322049"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">32</int>
|
||||
<reference key="object" ref="681499023"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">43</int>
|
||||
<reference key="object" ref="823054555"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="817997953"/>
|
||||
</object>
|
||||
<reference key="parent" ref="563959409"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">44</int>
|
||||
<reference key="object" ref="817997953"/>
|
||||
<reference key="parent" ref="823054555"/>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="flattenedProperties">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>-1.IBPluginDependency</string>
|
||||
<string>-2.IBPluginDependency</string>
|
||||
<string>-3.IBPluginDependency</string>
|
||||
<string>17.IBPluginDependency</string>
|
||||
<string>18.IBPluginDependency</string>
|
||||
<string>19.IBPluginDependency</string>
|
||||
<string>20.IBPluginDependency</string>
|
||||
<string>26.IBPluginDependency</string>
|
||||
<string>27.IBPluginDependency</string>
|
||||
<string>3.IBPluginDependency</string>
|
||||
<string>3.IBWindowTemplateEditedContentRect</string>
|
||||
<string>3.NSWindowTemplate.visibleAtLaunch</string>
|
||||
<string>32.IBPluginDependency</string>
|
||||
<string>4.IBPluginDependency</string>
|
||||
<string>43.IBPluginDependency</string>
|
||||
<string>44.IBPluginDependency</string>
|
||||
<string>5.IBPluginDependency</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>{{112, 709}, {515, 419}}</string>
|
||||
<boolean value="NO"/>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.WebKitIBPlugin</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="unlocalizedProperties">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference key="dict.sortedKeys" ref="0"/>
|
||||
<reference key="dict.values" ref="0"/>
|
||||
</object>
|
||||
<nil key="activeLocalization"/>
|
||||
<object class="NSMutableDictionary" key="localizations">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference key="dict.sortedKeys" ref="0"/>
|
||||
<reference key="dict.values" ref="0"/>
|
||||
</object>
|
||||
<nil key="sourceID"/>
|
||||
<int key="maxID">50</int>
|
||||
</object>
|
||||
<object class="IBClassDescriber" key="IBDocument.Classes">
|
||||
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="IBPartialClassDescription">
|
||||
<string key="className">GTMOAuth2WindowController</string>
|
||||
<string key="superclassName">NSWindowController</string>
|
||||
<object class="NSMutableDictionary" key="actions">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>closeWindow:</string>
|
||||
<string>toggleStorePasswordInKeychain:</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>id</string>
|
||||
<string>id</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="actionInfosByName">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>closeWindow:</string>
|
||||
<string>toggleStorePasswordInKeychain:</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="IBActionInfo">
|
||||
<string key="name">closeWindow:</string>
|
||||
<string key="candidateClassName">id</string>
|
||||
</object>
|
||||
<object class="IBActionInfo">
|
||||
<string key="name">toggleStorePasswordInKeychain:</string>
|
||||
<string key="candidateClassName">id</string>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="outlets">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>keychainCheckbox</string>
|
||||
<string>webBackButton</string>
|
||||
<string>webCloseButton</string>
|
||||
<string>webView</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>NSButton</string>
|
||||
<string>NSButton</string>
|
||||
<string>NSButton</string>
|
||||
<string>WebView</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>keychainCheckbox</string>
|
||||
<string>webBackButton</string>
|
||||
<string>webCloseButton</string>
|
||||
<string>webView</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="IBToOneOutletInfo">
|
||||
<string key="name">keychainCheckbox</string>
|
||||
<string key="candidateClassName">NSButton</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo">
|
||||
<string key="name">webBackButton</string>
|
||||
<string key="candidateClassName">NSButton</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo">
|
||||
<string key="name">webCloseButton</string>
|
||||
<string key="candidateClassName">NSButton</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo">
|
||||
<string key="name">webView</string>
|
||||
<string key="candidateClassName">WebView</string>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<object class="IBClassDescriptionSource" key="sourceIdentifier">
|
||||
<string key="majorKey">IBProjectSource</string>
|
||||
<string key="minorKey">./Classes/GTMOAuth2WindowController.h</string>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<int key="IBDocument.localizationMode">0</int>
|
||||
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
|
||||
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
|
||||
<integer value="1050" key="NS.object.0"/>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
|
||||
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string>
|
||||
<integer value="3000" key="NS.object.0"/>
|
||||
</object>
|
||||
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
|
||||
<int key="IBDocument.defaultPropertyAccessControl">3</int>
|
||||
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>NSGoLeftTemplate</string>
|
||||
<string>NSGoRightTemplate</string>
|
||||
<string>NSStopProgressTemplate</string>
|
||||
<string>NSSwitch</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>{9, 9}</string>
|
||||
<string>{9, 9}</string>
|
||||
<string>{11, 11}</string>
|
||||
<string>{15, 15}</string>
|
||||
</object>
|
||||
</object>
|
||||
</data>
|
||||
</archive>
|
||||
Reference in New Issue
Block a user