PageRenderTime 201ms CodeModel.GetById 10ms app.highlight 180ms RepoModel.GetById 2ms app.codeStats 0ms

/Source/externals/GData/Source/OAuth2/GTMOAuth2Authentication.m

http://google-email-uploader-mac.googlecode.com/
Objective C | 1327 lines | 1000 code | 208 blank | 119 comment | 150 complexity | e9c8636600a1908fa54f9fff67dfb0c6 MD5 | raw file
   1/* Copyright (c) 2011 Google Inc.
   2 *
   3 * Licensed under the Apache License, Version 2.0 (the "License");
   4 * you may not use this file except in compliance with the License.
   5 * You may obtain a copy of the License at
   6 *
   7 *     http://www.apache.org/licenses/LICENSE-2.0
   8 *
   9 * Unless required by applicable law or agreed to in writing, software
  10 * distributed under the License is distributed on an "AS IS" BASIS,
  11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 * See the License for the specific language governing permissions and
  13 * limitations under the License.
  14 */
  15
  16#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
  17
  18#import "GTMOAuth2Authentication.h"
  19
  20// Extern strings
  21
  22NSString *const kGTMOAuth2ServiceProviderGoogle = @"Google";
  23
  24NSString *const kGTMOAuth2ErrorDomain  = @"com.google.GTMOAuth2";
  25
  26NSString *const kGTMOAuth2ErrorMessageKey = @"error";
  27NSString *const kGTMOAuth2ErrorRequestKey = @"request";
  28NSString *const kGTMOAuth2ErrorJSONKey    = @"json";
  29
  30// Notifications
  31NSString *const kGTMOAuth2FetchStarted        = @"kGTMOAuth2FetchStarted";
  32NSString *const kGTMOAuth2FetchStopped        = @"kGTMOAuth2FetchStopped";
  33
  34NSString *const kGTMOAuth2FetcherKey          = @"fetcher";
  35NSString *const kGTMOAuth2FetchTypeKey        = @"FetchType";
  36NSString *const kGTMOAuth2FetchTypeToken      = @"token";
  37NSString *const kGTMOAuth2FetchTypeRefresh    = @"refresh";
  38NSString *const kGTMOAuth2FetchTypeAssertion  = @"assertion";
  39NSString *const kGTMOAuth2FetchTypeUserInfo   = @"userInfo";
  40
  41NSString *const kGTMOAuth2ErrorKey                  = @"error";
  42NSString *const kGTMOAuth2ErrorObjectKey            = @"kGTMOAuth2ErrorObjectKey";
  43
  44NSString *const kGTMOAuth2ErrorInvalidRequest       = @"invalid_request";
  45NSString *const kGTMOAuth2ErrorInvalidClient        = @"invalid_client";
  46NSString *const kGTMOAuth2ErrorInvalidGrant         = @"invalid_grant";
  47NSString *const kGTMOAuth2ErrorUnauthorizedClient   = @"unauthorized_client";
  48NSString *const kGTMOAuth2ErrorUnsupportedGrantType = @"unsupported_grant_type";
  49NSString *const kGTMOAuth2ErrorInvalidScope         = @"invalid_scope";
  50
  51NSString *const kGTMOAuth2UserSignedIn              = @"kGTMOAuth2UserSignedIn";
  52
  53NSString *const kGTMOAuth2AccessTokenRefreshed     = @"kGTMOAuth2AccessTokenRefreshed";
  54NSString *const kGTMOAuth2RefreshTokenChanged      = @"kGTMOAuth2RefreshTokenChanged";
  55NSString *const kGTMOAuth2AccessTokenRefreshFailed = @"kGTMOAuth2AccessTokenRefreshFailed";
  56
  57NSString *const kGTMOAuth2WebViewStartedLoading = @"kGTMOAuth2WebViewStartedLoading";
  58NSString *const kGTMOAuth2WebViewStoppedLoading = @"kGTMOAuth2WebViewStoppedLoading";
  59NSString *const kGTMOAuth2WebViewKey            = @"kGTMOAuth2WebViewKey";
  60NSString *const kGTMOAuth2WebViewStopKindKey    = @"kGTMOAuth2WebViewStopKindKey";
  61NSString *const kGTMOAuth2WebViewFinished       = @"finished";
  62NSString *const kGTMOAuth2WebViewFailed         = @"failed";
  63NSString *const kGTMOAuth2WebViewCancelled      = @"cancelled";
  64
  65NSString *const kGTMOAuth2NetworkLost         = @"kGTMOAuthNetworkLost";
  66NSString *const kGTMOAuth2NetworkFound        = @"kGTMOAuthNetworkFound";
  67
  68// standard OAuth keys
  69static NSString *const kOAuth2AccessTokenKey   = @"access_token";
  70static NSString *const kOAuth2RefreshTokenKey  = @"refresh_token";
  71static NSString *const kOAuth2ScopeKey         = @"scope";
  72static NSString *const kOAuth2ErrorKey         = @"error";
  73static NSString *const kOAuth2TokenTypeKey     = @"token_type";
  74static NSString *const kOAuth2ExpiresInKey     = @"expires_in";
  75static NSString *const kOAuth2CodeKey          = @"code";
  76static NSString *const kOAuth2AssertionKey     = @"assertion";
  77static NSString *const kOAuth2RefreshScopeKey  = @"refreshScope";
  78
  79// additional persistent keys
  80static NSString *const kServiceProviderKey     = @"serviceProvider";
  81static NSString *const kUserIDKey              = @"userID";
  82static NSString *const kUserEmailKey           = @"email";
  83static NSString *const kUserEmailIsVerifiedKey = @"isVerified";
  84
  85// fetcher keys
  86static NSString *const kTokenFetchDelegateKey = @"delegate";
  87static NSString *const kTokenFetchSelectorKey = @"sel";
  88
  89// If GTMNSJSONSerialization is available, it is used for formatting JSON
  90#if (TARGET_OS_MAC && !TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED < 1070)) || \
  91  (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED < 50000))
  92@interface GTMNSJSONSerialization : NSObject
  93+ (id)JSONObjectWithData:(NSData *)data options:(NSUInteger)opt error:(NSError **)error;
  94@end
  95#endif
  96
  97@interface GTMOAuth2ParserClass : NSObject
  98// just enough of SBJSON to be able to parse
  99- (id)objectWithString:(NSString*)repr error:(NSError**)error;
 100@end
 101
 102// wrapper class for requests needing authorization and their callbacks
 103@interface GTMOAuth2AuthorizationArgs : NSObject {
 104 @private
 105  NSMutableURLRequest *request_;
 106  id delegate_;
 107  SEL sel_;
 108  id completionHandler_;
 109  NSThread *thread_;
 110  NSError *error_;
 111}
 112
 113@property (retain) NSMutableURLRequest *request;
 114@property (retain) id delegate;
 115@property (assign) SEL selector;
 116@property (copy) id completionHandler;
 117@property (retain) NSThread *thread;
 118@property (retain) NSError *error;
 119
 120+ (GTMOAuth2AuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req
 121                                       delegate:(id)delegate
 122                                       selector:(SEL)sel
 123                              completionHandler:(id)completionHandler
 124                                         thread:(NSThread *)thread;
 125@end
 126
 127@implementation GTMOAuth2AuthorizationArgs
 128
 129@synthesize request = request_,
 130            delegate = delegate_,
 131            selector = sel_,
 132            completionHandler = completionHandler_,
 133            thread = thread_,
 134            error = error_;
 135
 136+ (GTMOAuth2AuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req
 137                                       delegate:(id)delegate
 138                                       selector:(SEL)sel
 139                              completionHandler:(id)completionHandler
 140                                         thread:(NSThread *)thread {
 141  GTMOAuth2AuthorizationArgs *obj;
 142  obj = [[[GTMOAuth2AuthorizationArgs alloc] init] autorelease];
 143  obj.request = req;
 144  obj.delegate = delegate;
 145  obj.selector = sel;
 146  obj.completionHandler = completionHandler;
 147  obj.thread = thread;
 148  return obj;
 149}
 150
 151- (void)dealloc {
 152  [request_ release];
 153  [delegate_ release];
 154  [completionHandler_ release];
 155  [thread_ release];
 156  [error_ release];
 157
 158  [super dealloc];
 159}
 160@end
 161
 162
 163@interface GTMOAuth2Authentication ()
 164
 165@property (retain) NSMutableArray *authorizationQueue;
 166@property (readonly) NSString *authorizationToken;
 167
 168- (void)setKeysForResponseJSONData:(NSData *)data;
 169
 170- (BOOL)authorizeRequestArgs:(GTMOAuth2AuthorizationArgs *)args;
 171
 172- (BOOL)authorizeRequestImmediateArgs:(GTMOAuth2AuthorizationArgs *)args;
 173
 174- (BOOL)shouldRefreshAccessToken;
 175
 176- (void)updateExpirationDate;
 177
 178- (void)tokenFetcher:(GTMHTTPFetcher *)fetcher
 179    finishedWithData:(NSData *)data
 180               error:(NSError *)error;
 181
 182- (void)auth:(GTMOAuth2Authentication *)auth
 183finishedRefreshWithFetcher:(GTMHTTPFetcher *)fetcher
 184       error:(NSError *)error;
 185
 186- (void)invokeCallbackArgs:(GTMOAuth2AuthorizationArgs *)args;
 187
 188+ (void)invokeDelegate:(id)delegate
 189              selector:(SEL)sel
 190                object:(id)obj1
 191                object:(id)obj2
 192                object:(id)obj3;
 193
 194+ (NSString *)unencodedOAuthParameterForString:(NSString *)str;
 195+ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict;
 196
 197+ (NSDictionary *)dictionaryWithResponseData:(NSData *)data;
 198
 199@end
 200
 201@implementation GTMOAuth2Authentication
 202
 203@synthesize clientID = clientID_,
 204            clientSecret = clientSecret_,
 205            redirectURI = redirectURI_,
 206            parameters = parameters_,
 207            authorizationTokenKey = authorizationTokenKey_,
 208            tokenURL = tokenURL_,
 209            expirationDate = expirationDate_,
 210            additionalTokenRequestParameters = additionalTokenRequestParameters_,
 211            additionalGrantTypeRequestParameters = additionalGrantTypeRequestParameters_,
 212            refreshFetcher = refreshFetcher_,
 213            fetcherService = fetcherService_,
 214            parserClass = parserClass_,
 215            shouldAuthorizeAllRequests = shouldAuthorizeAllRequests_,
 216            userData = userData_,
 217            properties = properties_,
 218            authorizationQueue = authorizationQueue_;
 219
 220// Response parameters
 221@dynamic accessToken,
 222         refreshToken,
 223         code,
 224         assertion,
 225         refreshScope,
 226         errorString,
 227         tokenType,
 228         scope,
 229         expiresIn,
 230         serviceProvider,
 231         userEmail,
 232         userEmailIsVerified;
 233
 234@dynamic canAuthorize;
 235
 236+ (id)authenticationWithServiceProvider:(NSString *)serviceProvider
 237                               tokenURL:(NSURL *)tokenURL
 238                            redirectURI:(NSString *)redirectURI
 239                               clientID:(NSString *)clientID
 240                           clientSecret:(NSString *)clientSecret {
 241  GTMOAuth2Authentication *obj = [[[self alloc] init] autorelease];
 242  obj.serviceProvider = serviceProvider;
 243  obj.tokenURL = tokenURL;
 244  obj.redirectURI = redirectURI;
 245  obj.clientID = clientID;
 246  obj.clientSecret = clientSecret;
 247  return obj;
 248}
 249
 250- (id)init {
 251  self = [super init];
 252  if (self) {
 253    authorizationQueue_ = [[NSMutableArray alloc] init];
 254    parameters_ = [[NSMutableDictionary alloc] init];
 255  }
 256  return self;
 257}
 258
 259- (NSString *)description {
 260  NSArray *props = [NSArray arrayWithObjects:@"accessToken", @"refreshToken",
 261                    @"code", @"assertion", @"expirationDate", @"errorString",
 262                    nil];
 263  NSMutableString *valuesStr = [NSMutableString string];
 264  NSString *separator = @"";
 265  for (NSString *prop in props) {
 266    id result = [self valueForKey:prop];
 267    if (result) {
 268      [valuesStr appendFormat:@"%@%@=\"%@\"", separator, prop, result];
 269      separator = @", ";
 270    }
 271  }
 272
 273  return [NSString stringWithFormat:@"%@ %p: {%@}",
 274          [self class], self, valuesStr];
 275}
 276
 277- (void)dealloc {
 278  [clientID_ release];
 279  [clientSecret_ release];
 280  [redirectURI_ release];
 281  [parameters_ release];
 282  [authorizationTokenKey_ release];
 283  [tokenURL_ release];
 284  [expirationDate_ release];
 285  [additionalTokenRequestParameters_ release];
 286  [additionalGrantTypeRequestParameters_ release];
 287  [refreshFetcher_ release];
 288  [authorizationQueue_ release];
 289  [userData_ release];
 290  [properties_ release];
 291
 292  [super dealloc];
 293}
 294
 295#pragma mark -
 296
 297- (void)setKeysForResponseDictionary:(NSDictionary *)dict {
 298  if (dict == nil) return;
 299
 300  // If a new code or access token is being set, remove the old expiration
 301  NSString *newCode = [dict objectForKey:kOAuth2CodeKey];
 302  NSString *newAccessToken = [dict objectForKey:kOAuth2AccessTokenKey];
 303  if (newCode || newAccessToken) {
 304    self.expiresIn = nil;
 305  }
 306
 307  BOOL didRefreshTokenChange = NO;
 308  NSString *refreshToken = [dict objectForKey:kOAuth2RefreshTokenKey];
 309  if (refreshToken) {
 310    NSString *priorRefreshToken = self.refreshToken;
 311
 312    if (priorRefreshToken != refreshToken
 313        && (priorRefreshToken == nil
 314            || ![priorRefreshToken isEqual:refreshToken])) {
 315          didRefreshTokenChange = YES;
 316        }
 317  }
 318
 319  [self.parameters addEntriesFromDictionary:dict];
 320  [self updateExpirationDate];
 321
 322  if (didRefreshTokenChange) {
 323    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
 324    [nc postNotificationName:kGTMOAuth2RefreshTokenChanged
 325                      object:self
 326                    userInfo:nil];
 327  }
 328  // NSLog(@"keys set ----------------------------\n%@", dict);
 329}
 330
 331- (void)setKeysForResponseString:(NSString *)str {
 332  NSDictionary *dict = [[self class] dictionaryWithResponseString:str];
 333  [self setKeysForResponseDictionary:dict];
 334}
 335
 336- (void)setKeysForResponseJSONData:(NSData *)data {
 337  NSDictionary *dict = [[self class] dictionaryWithJSONData:data];
 338  [self setKeysForResponseDictionary:dict];
 339}
 340
 341+ (NSDictionary *)dictionaryWithJSONData:(NSData *)data {
 342  NSMutableDictionary *obj = nil;
 343  NSError *error = nil;
 344
 345  Class serializer = NSClassFromString(@"NSJSONSerialization");
 346  if (serializer) {
 347    const NSUInteger kOpts = (1UL << 0); // NSJSONReadingMutableContainers
 348    obj = [serializer JSONObjectWithData:data
 349                                 options:kOpts
 350                                   error:&error];
 351#if DEBUG
 352    if (error) {
 353      NSString *str = [[[NSString alloc] initWithData:data
 354                                             encoding:NSUTF8StringEncoding] autorelease];
 355      NSLog(@"NSJSONSerialization error %@ parsing %@",
 356            error, str);
 357    }
 358#endif
 359    return obj;
 360  } else {
 361    // try SBJsonParser or SBJSON
 362    Class jsonParseClass = NSClassFromString(@"SBJsonParser");
 363    if (!jsonParseClass) {
 364      jsonParseClass = NSClassFromString(@"SBJSON");
 365    }
 366    if (jsonParseClass) {
 367      GTMOAuth2ParserClass *parser = [[[jsonParseClass alloc] init] autorelease];
 368      NSString *jsonStr = [[[NSString alloc] initWithData:data
 369                                                 encoding:NSUTF8StringEncoding] autorelease];
 370      if (jsonStr) {
 371        obj = [parser objectWithString:jsonStr error:&error];
 372#if DEBUG
 373        if (error) {
 374          NSLog(@"%@ error %@ parsing %@", NSStringFromClass(jsonParseClass),
 375                error, jsonStr);
 376        }
 377#endif
 378        return obj;
 379      }
 380    } else {
 381#if DEBUG
 382      NSAssert(0, @"GTMOAuth2Authentication: No parser available");
 383#endif
 384    }
 385  }
 386  return nil;
 387}
 388
 389#pragma mark Authorizing Requests
 390
 391// General entry point for authorizing requests
 392
 393#if NS_BLOCKS_AVAILABLE
 394// Authorizing with a completion block
 395- (void)authorizeRequest:(NSMutableURLRequest *)request
 396       completionHandler:(void (^)(NSError *error))handler {
 397
 398  GTMOAuth2AuthorizationArgs *args;
 399  args = [GTMOAuth2AuthorizationArgs argsWithRequest:request
 400                                            delegate:nil
 401                                            selector:NULL
 402                                   completionHandler:handler
 403                                              thread:[NSThread currentThread]];
 404  [self authorizeRequestArgs:args];
 405}
 406#endif
 407
 408// Authorizing with a callback selector
 409//
 410// Selector has the signature
 411//   - (void)authentication:(GTMOAuth2Authentication *)auth
 412//                  request:(NSMutableURLRequest *)request
 413//        finishedWithError:(NSError *)error;
 414- (void)authorizeRequest:(NSMutableURLRequest *)request
 415                delegate:(id)delegate
 416       didFinishSelector:(SEL)sel {
 417  GTMAssertSelectorNilOrImplementedWithArgs(delegate, sel,
 418                                            @encode(GTMOAuth2Authentication *),
 419                                            @encode(NSMutableURLRequest *),
 420                                            @encode(NSError *), 0);
 421
 422  GTMOAuth2AuthorizationArgs *args;
 423  args = [GTMOAuth2AuthorizationArgs argsWithRequest:request
 424                                            delegate:delegate
 425                                            selector:sel
 426                                   completionHandler:nil
 427                                              thread:[NSThread currentThread]];
 428  [self authorizeRequestArgs:args];
 429}
 430
 431// Internal routine common to delegate and block invocations
 432- (BOOL)authorizeRequestArgs:(GTMOAuth2AuthorizationArgs *)args {
 433  BOOL didAttempt = NO;
 434
 435  @synchronized(authorizationQueue_) {
 436
 437    BOOL shouldRefresh = [self shouldRefreshAccessToken];
 438
 439    if (shouldRefresh) {
 440      // attempt to refresh now; once we have a fresh access token, we will
 441      // authorize the request and call back to the user
 442      didAttempt = YES;
 443
 444      if (self.refreshFetcher == nil) {
 445        // there's not already a refresh pending
 446        SEL finishedSel = @selector(auth:finishedRefreshWithFetcher:error:);
 447        self.refreshFetcher = [self beginTokenFetchWithDelegate:self
 448                                              didFinishSelector:finishedSel];
 449        if (self.refreshFetcher) {
 450          [authorizationQueue_ addObject:args];
 451        }
 452      } else {
 453        // there's already a refresh pending
 454        [authorizationQueue_ addObject:args];
 455      }
 456    }
 457
 458    if (!shouldRefresh || self.refreshFetcher == nil) {
 459      // we're not fetching a new access token, so we can authorize the request
 460      // now
 461      didAttempt = [self authorizeRequestImmediateArgs:args];
 462    }
 463  }
 464  return didAttempt;
 465}
 466
 467- (void)auth:(GTMOAuth2Authentication *)auth
 468finishedRefreshWithFetcher:(GTMHTTPFetcher *)fetcher
 469       error:(NSError *)error {
 470  @synchronized(authorizationQueue_) {
 471    // If there's an error, we want to try using the old access token anyway,
 472    // in case it's a backend problem preventing refresh, in which case
 473    // access tokens past their expiration date may still work
 474
 475    self.refreshFetcher = nil;
 476
 477    // Swap in a new auth queue in case the callbacks try to immediately auth
 478    // another request
 479    NSArray *pendingAuthQueue = [NSArray arrayWithArray:authorizationQueue_];
 480    [authorizationQueue_ removeAllObjects];
 481
 482    BOOL hasAccessToken = ([self.accessToken length] > 0);
 483
 484    NSString *noteName;
 485    NSDictionary *userInfo = nil;
 486    if (hasAccessToken && error == nil) {
 487      // Successful refresh.
 488      noteName = kGTMOAuth2AccessTokenRefreshed;
 489      userInfo = nil;
 490    } else {
 491      // Google's OAuth 2 implementation returns a 400 with JSON body
 492      // containing error key "invalid_grant" to indicate the refresh token
 493      // is invalid or has been revoked by the user.  We'll promote the
 494      // JSON error key's value for easy inspection by the observer.
 495      noteName = kGTMOAuth2AccessTokenRefreshFailed;
 496      NSString *jsonErr = nil;
 497      if ([error code] == kGTMHTTPFetcherStatusBadRequest) {
 498        NSDictionary *json = [[error userInfo] objectForKey:kGTMOAuth2ErrorJSONKey];
 499        jsonErr = [json objectForKey:kGTMOAuth2ErrorMessageKey];
 500      }
 501      // error and jsonErr may be nil
 502      userInfo = [NSMutableDictionary dictionary];
 503      [userInfo setValue:error forKey:kGTMOAuth2ErrorObjectKey];
 504      [userInfo setValue:jsonErr forKey:kGTMOAuth2ErrorMessageKey];
 505    }
 506    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
 507    [nc postNotificationName:noteName
 508                      object:self
 509                    userInfo:userInfo];
 510
 511    for (GTMOAuth2AuthorizationArgs *args in pendingAuthQueue) {
 512      if (!hasAccessToken && args.error == nil) {
 513        args.error = error;
 514      }
 515
 516      [self authorizeRequestImmediateArgs:args];
 517    }
 518  }
 519}
 520
 521- (BOOL)isAuthorizingRequest:(NSURLRequest *)request {
 522  BOOL wasFound = NO;
 523  @synchronized(authorizationQueue_) {
 524    for (GTMOAuth2AuthorizationArgs *args in authorizationQueue_) {
 525      if ([args request] == request) {
 526        wasFound = YES;
 527        break;
 528      }
 529    }
 530  }
 531  return wasFound;
 532}
 533
 534- (BOOL)isAuthorizedRequest:(NSURLRequest *)request {
 535  NSString *authStr = [request valueForHTTPHeaderField:@"Authorization"];
 536  return ([authStr length] > 0);
 537}
 538
 539- (void)stopAuthorization {
 540  @synchronized(authorizationQueue_) {
 541    [authorizationQueue_ removeAllObjects];
 542
 543    [self.refreshFetcher stopFetching];
 544    self.refreshFetcher = nil;
 545  }
 546}
 547
 548- (void)stopAuthorizationForRequest:(NSURLRequest *)request {
 549  @synchronized(authorizationQueue_) {
 550    NSUInteger argIndex = 0;
 551    BOOL found = NO;
 552    for (GTMOAuth2AuthorizationArgs *args in authorizationQueue_) {
 553      if ([args request] == request) {
 554        found = YES;
 555        break;
 556      }
 557      argIndex++;
 558    }
 559
 560    if (found) {
 561      [authorizationQueue_ removeObjectAtIndex:argIndex];
 562
 563      // If the queue is now empty, go ahead and stop the fetcher.
 564      if ([authorizationQueue_ count] == 0) {
 565        [self stopAuthorization];
 566      }
 567    }
 568  }
 569}
 570
 571- (BOOL)authorizeRequestImmediateArgs:(GTMOAuth2AuthorizationArgs *)args {
 572  // This authorization entry point never attempts to refresh the access token,
 573  // but does call the completion routine
 574
 575  NSMutableURLRequest *request = args.request;
 576
 577  NSURL *requestURL = [request URL];
 578  NSString *scheme = [requestURL scheme];
 579  BOOL isAuthorizableRequest = self.shouldAuthorizeAllRequests
 580    || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame
 581    || [requestURL isFileURL];
 582  if (!isAuthorizableRequest) {
 583    // Request is not https, so may be insecure
 584    //
 585    // The NSError will be created below
 586#if DEBUG
 587    NSLog(@"Cannot authorize request with scheme %@ (%@)", scheme, request);
 588#endif
 589  }
 590
 591  // Get the access token.
 592  NSString *accessToken = self.authorizationToken;
 593  if (isAuthorizableRequest && [accessToken length] > 0) {
 594    if (request) {
 595      // we have a likely valid access token
 596      NSString *value = [NSString stringWithFormat:@"%s %@",
 597                         GTM_OAUTH2_BEARER, accessToken];
 598      [request setValue:value forHTTPHeaderField:@"Authorization"];
 599    }
 600
 601    // We've authorized the request, even if the previous refresh
 602    // failed with an error
 603    args.error = nil;
 604  } else if (args.error == nil) {
 605    NSDictionary *userInfo = nil;
 606    if (request) {
 607      userInfo = [NSDictionary dictionaryWithObject:request
 608                                             forKey:kGTMOAuth2ErrorRequestKey];
 609    }
 610    NSInteger code = (isAuthorizableRequest ?
 611                      kGTMOAuth2ErrorAuthorizationFailed :
 612                      kGTMOAuth2ErrorUnauthorizableRequest);
 613    args.error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
 614                                     code:code
 615                                 userInfo:userInfo];
 616  }
 617
 618  // Invoke any callbacks on the proper thread
 619  if (args.delegate || args.completionHandler) {
 620    NSThread *targetThread = args.thread;
 621    BOOL isSameThread = [targetThread isEqual:[NSThread currentThread]];
 622
 623    if (isSameThread) {
 624      [self invokeCallbackArgs:args];
 625    } else {
 626      SEL sel = @selector(invokeCallbackArgs:);
 627      NSOperationQueue *delegateQueue = self.fetcherService.delegateQueue;
 628      if (delegateQueue) {
 629        NSInvocationOperation *op;
 630        op = [[[NSInvocationOperation alloc] initWithTarget:self
 631                                                   selector:sel
 632                                                     object:args] autorelease];
 633        [delegateQueue addOperation:op];
 634      } else {
 635        [self performSelector:sel
 636                     onThread:targetThread
 637                   withObject:args
 638                waitUntilDone:NO];
 639      }
 640    }
 641  }
 642
 643  BOOL didAuth = (args.error == nil);
 644  return didAuth;
 645}
 646
 647- (void)invokeCallbackArgs:(GTMOAuth2AuthorizationArgs *)args {
 648  // Invoke the callbacks
 649  NSError *error = args.error;
 650
 651  id delegate = args.delegate;
 652  SEL sel = args.selector;
 653  if (delegate && sel) {
 654    NSMutableURLRequest *request = args.request;
 655
 656    NSMethodSignature *sig = [delegate methodSignatureForSelector:sel];
 657    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
 658    [invocation setSelector:sel];
 659    [invocation setTarget:delegate];
 660    [invocation setArgument:&self atIndex:2];
 661    [invocation setArgument:&request atIndex:3];
 662    [invocation setArgument:&error atIndex:4];
 663    [invocation invoke];
 664  }
 665
 666#if NS_BLOCKS_AVAILABLE
 667  id handler = args.completionHandler;
 668  if (handler) {
 669    void (^authCompletionBlock)(NSError *) = handler;
 670    authCompletionBlock(error);
 671  }
 672#endif
 673}
 674
 675- (BOOL)authorizeRequest:(NSMutableURLRequest *)request {
 676  // Entry point for synchronous authorization mechanisms
 677  GTMOAuth2AuthorizationArgs *args;
 678  args = [GTMOAuth2AuthorizationArgs argsWithRequest:request
 679                                            delegate:nil
 680                                            selector:NULL
 681                                   completionHandler:nil
 682                                              thread:[NSThread currentThread]];
 683  return [self authorizeRequestImmediateArgs:args];
 684}
 685
 686- (BOOL)canAuthorize {
 687  NSString *token = self.refreshToken;
 688  if (token == nil) {
 689    // For services which do not support refresh tokens, we'll just check
 690    // the access token.
 691    token = self.authorizationToken;
 692  }
 693  BOOL canAuth = [token length] > 0;
 694  return canAuth;
 695}
 696
 697- (BOOL)shouldRefreshAccessToken {
 698  // We should refresh the access token when it's missing or nearly expired
 699  // and we have a refresh token
 700  BOOL shouldRefresh = NO;
 701  NSString *accessToken = self.accessToken;
 702  NSString *refreshToken = self.refreshToken;
 703  NSString *assertion = self.assertion;
 704  NSString *code = self.code;
 705
 706  BOOL hasRefreshToken = ([refreshToken length] > 0);
 707  BOOL hasAccessToken = ([accessToken length] > 0);
 708  BOOL hasAssertion = ([assertion length] > 0);
 709  BOOL hasCode = ([code length] > 0);
 710
 711  // Determine if we need to refresh the access token
 712  if (hasRefreshToken || hasAssertion || hasCode) {
 713    if (!hasAccessToken) {
 714      shouldRefresh = YES;
 715    } else {
 716      // We'll consider the token expired if it expires 60 seconds from now
 717      // or earlier
 718      NSDate *expirationDate = self.expirationDate;
 719      NSTimeInterval timeToExpire = [expirationDate timeIntervalSinceNow];
 720      if (expirationDate == nil || timeToExpire < 60.0) {
 721        // access token has expired, or will in a few seconds
 722        shouldRefresh = YES;
 723      }
 724    }
 725  }
 726  return shouldRefresh;
 727}
 728
 729- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
 730  // If there is a refresh fetcher pending, wait for it.
 731  //
 732  // This is only intended for unit test or for use in command-line tools.
 733  GTMHTTPFetcher *fetcher = self.refreshFetcher;
 734  [fetcher waitForCompletionWithTimeout:timeoutInSeconds];
 735}
 736
 737#pragma mark Token Fetch
 738
 739- (NSString *)userAgent {
 740  NSBundle *bundle = [NSBundle mainBundle];
 741  NSString *appID = [bundle bundleIdentifier];
 742
 743  NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
 744  if (version == nil) {
 745    version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"];
 746  }
 747
 748  if (appID && version) {
 749    appID = [appID stringByAppendingFormat:@"/%@", version];
 750  }
 751
 752  NSString *userAgent = @"gtm-oauth2";
 753  if (appID) {
 754    userAgent = [userAgent stringByAppendingFormat:@" %@", appID];
 755  }
 756  return userAgent;
 757}
 758
 759- (GTMHTTPFetcher *)beginTokenFetchWithDelegate:(id)delegate
 760                              didFinishSelector:(SEL)finishedSel {
 761
 762  NSMutableDictionary *paramsDict = [NSMutableDictionary dictionary];
 763
 764  NSString *fetchType;
 765
 766  NSString *refreshToken = self.refreshToken;
 767  NSString *code = self.code;
 768  NSString *assertion = self.assertion;
 769  NSString *grantType = nil;
 770  
 771  if (refreshToken) {
 772    // We have a refresh token
 773    grantType = @"refresh_token";
 774    [paramsDict setObject:refreshToken forKey:@"refresh_token"];
 775
 776    NSString *refreshScope = self.refreshScope;
 777    if ([refreshScope length] > 0) {
 778      [paramsDict setObject:refreshScope forKey:@"scope"];
 779    }
 780
 781    fetchType = kGTMOAuth2FetchTypeRefresh;
 782  } else if (code) {
 783    // We have a code string
 784    grantType = @"authorization_code";
 785    [paramsDict setObject:code forKey:@"code"];
 786
 787    NSString *redirectURI = self.redirectURI;
 788    if ([redirectURI length] > 0) {
 789      [paramsDict setObject:redirectURI forKey:@"redirect_uri"];
 790    }
 791    
 792    NSString *scope = self.scope;
 793    if ([scope length] > 0) {
 794      [paramsDict setObject:scope forKey:@"scope"];
 795    }
 796    
 797    fetchType = kGTMOAuth2FetchTypeToken;
 798  } else if (assertion) {
 799    // We have an assertion string
 800    grantType = @"http://oauth.net/grant_type/jwt/1.0/bearer";
 801    [paramsDict setObject:assertion forKey:@"assertion"];
 802    fetchType = kGTMOAuth2FetchTypeAssertion;
 803  } else {
 804#if DEBUG
 805    NSAssert(0, @"unexpected lack of code or refresh token for fetching");
 806#endif
 807    return nil;
 808  }
 809  [paramsDict setObject:grantType forKey:@"grant_type"];
 810
 811  NSString *clientID = self.clientID;
 812  if ([clientID length] > 0) {
 813    [paramsDict setObject:clientID forKey:@"client_id"];
 814  }
 815
 816  NSString *clientSecret = self.clientSecret;
 817  if ([clientSecret length] > 0) {
 818    [paramsDict setObject:clientSecret forKey:@"client_secret"];
 819  }
 820
 821  NSDictionary *additionalParams = self.additionalTokenRequestParameters;
 822  if (additionalParams) {
 823    [paramsDict addEntriesFromDictionary:additionalParams];
 824  }
 825  NSDictionary *grantTypeParams =
 826    [self.additionalGrantTypeRequestParameters objectForKey:grantType];
 827  if (grantTypeParams) {
 828    [paramsDict addEntriesFromDictionary:grantTypeParams];
 829  }
 830
 831  NSString *paramStr = [[self class] encodedQueryParametersForDictionary:paramsDict];
 832  NSData *paramData = [paramStr dataUsingEncoding:NSUTF8StringEncoding];
 833
 834  NSURL *tokenURL = self.tokenURL;
 835
 836  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:tokenURL];
 837  [request setValue:@"application/x-www-form-urlencoded"
 838 forHTTPHeaderField:@"Content-Type"];
 839
 840  NSString *userAgent = [self userAgent];
 841  [request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
 842
 843  GTMHTTPFetcher *fetcher;
 844  id <GTMHTTPFetcherServiceProtocol> fetcherService = self.fetcherService;
 845  if (fetcherService) {
 846    fetcher = [fetcherService fetcherWithRequest:request];
 847
 848    // Don't use an authorizer for an auth token fetch
 849    fetcher.authorizer = nil;
 850  } else {
 851    fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
 852  }
 853
 854#if !STRIP_GTM_FETCH_LOGGING
 855  // The user email address is known at token refresh time, not during the initial code exchange.
 856  NSString *userEmail = [self userEmail];
 857  NSString *forStr = userEmail ? [NSString stringWithFormat:@"for \"%@\"", userEmail] : @"";
 858  [fetcher setCommentWithFormat:@"GTMOAuth2 %@ fetch to %@ %@", fetchType, [tokenURL host], forStr];
 859#endif
 860
 861  fetcher.postData = paramData;
 862  fetcher.retryEnabled = YES;
 863  fetcher.maxRetryInterval = 15.0;
 864
 865  // Fetcher properties will retain the delegate
 866  [fetcher setProperty:delegate forKey:kTokenFetchDelegateKey];
 867  if (finishedSel) {
 868    NSString *selStr = NSStringFromSelector(finishedSel);
 869    [fetcher setProperty:selStr forKey:kTokenFetchSelectorKey];
 870  }
 871
 872  if ([fetcher beginFetchWithDelegate:self
 873                    didFinishSelector:@selector(tokenFetcher:finishedWithData:error:)]) {
 874    // Fetch began
 875    [self notifyFetchIsRunning:YES fetcher:fetcher type:fetchType];
 876    return fetcher;
 877  } else {
 878    // Failed to start fetching; typically a URL issue
 879    NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
 880                                         code:-1
 881                                     userInfo:nil];
 882    [[self class] invokeDelegate:delegate
 883                        selector:finishedSel
 884                          object:self
 885                          object:nil
 886                          object:error];
 887    return nil;
 888  }
 889}
 890
 891- (void)tokenFetcher:(GTMHTTPFetcher *)fetcher
 892    finishedWithData:(NSData *)data
 893               error:(NSError *)error {
 894  [self notifyFetchIsRunning:NO fetcher:fetcher type:nil];
 895
 896  NSDictionary *responseHeaders = [fetcher responseHeaders];
 897  NSString *responseType = [responseHeaders valueForKey:@"Content-Type"];
 898  BOOL isResponseJSON = [responseType hasPrefix:@"application/json"];
 899  BOOL hasData = ([data length] > 0);
 900
 901  if (error) {
 902    // Failed. If the error body is JSON, parse it and add it to the error's
 903    // userInfo dictionary.
 904    if (hasData) {
 905      if (isResponseJSON) {
 906        NSDictionary *errorJson = [[self class] dictionaryWithJSONData:data];
 907        if ([errorJson count] > 0) {
 908#if DEBUG
 909          NSLog(@"Error %@\nError data:\n%@", error, errorJson);
 910#endif
 911          // Add the JSON error body to the userInfo of the error
 912          NSMutableDictionary *userInfo;
 913          userInfo = [NSMutableDictionary dictionaryWithObject:errorJson
 914                                                        forKey:kGTMOAuth2ErrorJSONKey];
 915          NSDictionary *prevUserInfo = [error userInfo];
 916          if (prevUserInfo) {
 917            [userInfo addEntriesFromDictionary:prevUserInfo];
 918          }
 919          error = [NSError errorWithDomain:[error domain]
 920                                      code:[error code]
 921                                  userInfo:userInfo];
 922        }
 923      }
 924    }
 925  } else {
 926    // Succeeded; we have the requested token.
 927#if DEBUG
 928    NSAssert(hasData, @"data missing in token response");
 929#endif
 930
 931    if (hasData) {
 932      if (isResponseJSON) {
 933        [self setKeysForResponseJSONData:data];
 934      } else {
 935        // Support for legacy token servers that return form-urlencoded data
 936        NSString *dataStr = [[[NSString alloc] initWithData:data
 937                                                   encoding:NSUTF8StringEncoding] autorelease];
 938        [self setKeysForResponseString:dataStr];
 939      }
 940
 941#if DEBUG
 942      // Watch for token exchanges that return a non-bearer or unlabeled token
 943      NSString *tokenType = [self tokenType];
 944      if (tokenType == nil
 945          || [tokenType caseInsensitiveCompare:@"bearer"] != NSOrderedSame) {
 946        NSLog(@"GTMOAuth2: Unexpected token type: %@", tokenType);
 947      }
 948#endif
 949    }
 950  }
 951
 952  id delegate = [fetcher propertyForKey:kTokenFetchDelegateKey];
 953  SEL sel = NULL;
 954  NSString *selStr = [fetcher propertyForKey:kTokenFetchSelectorKey];
 955  if (selStr) sel = NSSelectorFromString(selStr);
 956
 957  [[self class] invokeDelegate:delegate
 958                      selector:sel
 959                        object:self
 960                        object:fetcher
 961                        object:error];
 962
 963  // Prevent a circular reference from retaining the delegate
 964  [fetcher setProperty:nil forKey:kTokenFetchDelegateKey];
 965}
 966
 967#pragma mark Fetch Notifications
 968
 969- (void)notifyFetchIsRunning:(BOOL)isStarting
 970                     fetcher:(GTMHTTPFetcher *)fetcher
 971                        type:(NSString *)fetchType {
 972  NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
 973
 974  NSString *name = (isStarting ? kGTMOAuth2FetchStarted : kGTMOAuth2FetchStopped);
 975  NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
 976                        fetcher, kGTMOAuth2FetcherKey,
 977                        fetchType, kGTMOAuth2FetchTypeKey, // fetchType may be nil
 978                        nil];
 979  [nc postNotificationName:name
 980                    object:self
 981                  userInfo:dict];
 982}
 983
 984#pragma mark Persistent Response Strings
 985
 986- (void)setKeysForPersistenceResponseString:(NSString *)str {
 987  // All persistence keys can be set directly as if returned by a server
 988  [self setKeysForResponseString:str];
 989}
 990
 991// This returns a "response string" that can be passed later to
 992// setKeysForResponseString: to reuse an old access token in a new auth object
 993- (NSString *)persistenceResponseString {
 994  NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:4];
 995
 996  NSString *refreshToken = self.refreshToken;
 997  NSString *accessToken = nil;
 998  if (refreshToken == nil) {
 999    // We store the access token only for services that do not support refresh
1000    // tokens; otherwise, we assume the access token is too perishable to
1001    // be worth storing
1002    accessToken = self.accessToken;
1003  }
1004
1005  // Any nil values will not set a dictionary entry
1006  [dict setValue:refreshToken forKey:kOAuth2RefreshTokenKey];
1007  [dict setValue:accessToken forKey:kOAuth2AccessTokenKey];
1008  [dict setValue:self.serviceProvider forKey:kServiceProviderKey];
1009  [dict setValue:self.userID forKey:kUserIDKey];
1010  [dict setValue:self.userEmail forKey:kUserEmailKey];
1011  [dict setValue:self.userEmailIsVerified forKey:kUserEmailIsVerifiedKey];
1012  [dict setValue:self.scope forKey:kOAuth2ScopeKey];
1013
1014  NSString *result = [[self class] encodedQueryParametersForDictionary:dict];
1015  return result;
1016}
1017
1018- (BOOL)primeForRefresh {
1019  if (self.refreshToken == nil) {
1020    // Cannot refresh without a refresh token
1021    return NO;
1022  }
1023  self.accessToken = nil;
1024  self.expiresIn = nil;
1025  self.expirationDate = nil;
1026  self.errorString = nil;
1027  return YES;
1028}
1029
1030- (void)reset {
1031  // Reset all per-authorization values
1032  self.code = nil;
1033  self.accessToken = nil;
1034  self.refreshToken = nil;
1035  self.assertion = nil;
1036  self.expiresIn = nil;
1037  self.errorString = nil;
1038  self.expirationDate = nil;
1039  self.userEmail = nil;
1040  self.userEmailIsVerified = nil;
1041  self.authorizationTokenKey = nil;
1042}
1043
1044#pragma mark Accessors for Response Parameters
1045
1046- (NSString *)authorizationToken {
1047  // The token used for authorization is typically the access token unless
1048  // the user has specified that an alternative parameter be used.
1049  NSString *authorizationToken;
1050  NSString *authTokenKey = self.authorizationTokenKey;
1051  if (authTokenKey != nil) {
1052    authorizationToken = [self.parameters objectForKey:authTokenKey];
1053  } else {
1054    authorizationToken = self.accessToken;
1055  }
1056  return authorizationToken;
1057}
1058
1059- (NSString *)accessToken {
1060  return [self.parameters objectForKey:kOAuth2AccessTokenKey];
1061}
1062
1063- (void)setAccessToken:(NSString *)str {
1064  [self.parameters setValue:str forKey:kOAuth2AccessTokenKey];
1065}
1066
1067- (NSString *)refreshToken {
1068  return [self.parameters objectForKey:kOAuth2RefreshTokenKey];
1069}
1070
1071- (void)setRefreshToken:(NSString *)str {
1072  [self.parameters setValue:str forKey:kOAuth2RefreshTokenKey];
1073}
1074
1075- (NSString *)code {
1076  return [self.parameters objectForKey:kOAuth2CodeKey];
1077}
1078
1079- (void)setCode:(NSString *)str {
1080  [self.parameters setValue:str forKey:kOAuth2CodeKey];
1081}
1082
1083- (NSString *)assertion {
1084  return [self.parameters objectForKey:kOAuth2AssertionKey];
1085}
1086
1087- (void)setAssertion:(NSString *)str {
1088  [self.parameters setValue:str forKey:kOAuth2AssertionKey];
1089}
1090
1091- (NSString *)refreshScope {
1092  return [self.parameters objectForKey:kOAuth2RefreshScopeKey];
1093}
1094
1095- (void)setRefreshScope:(NSString *)str {
1096  [self.parameters setValue:str forKey:kOAuth2RefreshScopeKey];
1097}
1098
1099- (NSString *)errorString {
1100  return [self.parameters objectForKey:kOAuth2ErrorKey];
1101}
1102
1103- (void)setErrorString:(NSString *)str {
1104  [self.parameters setValue:str forKey:kOAuth2ErrorKey];
1105}
1106
1107- (NSString *)tokenType {
1108  return [self.parameters objectForKey:kOAuth2TokenTypeKey];
1109}
1110
1111- (void)setTokenType:(NSString *)str {
1112  [self.parameters setValue:str forKey:kOAuth2TokenTypeKey];
1113}
1114
1115- (NSString *)scope {
1116  return [self.parameters objectForKey:kOAuth2ScopeKey];
1117}
1118
1119- (void)setScope:(NSString *)str {
1120  [self.parameters setValue:str forKey:kOAuth2ScopeKey];
1121}
1122
1123- (NSNumber *)expiresIn {
1124  id value = [self.parameters objectForKey:kOAuth2ExpiresInKey];
1125  if ([value isKindOfClass:[NSString class]]) {
1126    value = [NSNumber numberWithInteger:[value integerValue]];
1127  }
1128  return value;
1129}
1130
1131- (void)setExpiresIn:(NSNumber *)num {
1132  [self.parameters setValue:num forKey:kOAuth2ExpiresInKey];
1133  [self updateExpirationDate];
1134}
1135
1136- (void)updateExpirationDate {
1137  // Update our absolute expiration time to something close to when
1138  // the server expects the expiration
1139  NSDate *date = nil;
1140  NSNumber *expiresIn = self.expiresIn;
1141  if (expiresIn) {
1142    unsigned long deltaSeconds = [expiresIn unsignedLongValue];
1143    if (deltaSeconds > 0) {
1144      date = [NSDate dateWithTimeIntervalSinceNow:deltaSeconds];
1145    }
1146  }
1147  self.expirationDate = date;
1148}
1149
1150//
1151// Keys custom to this class, not part of OAuth 2
1152//
1153
1154- (NSString *)serviceProvider {
1155  return [self.parameters objectForKey:kServiceProviderKey];
1156}
1157
1158- (void)setServiceProvider:(NSString *)str {
1159  [self.parameters setValue:str forKey:kServiceProviderKey];
1160}
1161
1162- (NSString *)userID {
1163  return [self.parameters objectForKey:kUserIDKey];
1164}
1165
1166- (void)setUserID:(NSString *)str {
1167  [self.parameters setValue:str forKey:kUserIDKey];
1168}
1169
1170- (NSString *)userEmail {
1171  return [self.parameters objectForKey:kUserEmailKey];
1172}
1173
1174- (void)setUserEmail:(NSString *)str {
1175  [self.parameters setValue:str forKey:kUserEmailKey];
1176}
1177
1178- (NSString *)userEmailIsVerified {
1179  return [self.parameters objectForKey:kUserEmailIsVerifiedKey];
1180}
1181
1182- (void)setUserEmailIsVerified:(NSString *)str {
1183  [self.parameters setValue:str forKey:kUserEmailIsVerifiedKey];
1184}
1185
1186#pragma mark User Properties
1187
1188- (void)setProperty:(id)obj forKey:(NSString *)key {
1189  if (obj == nil) {
1190    // User passed in nil, so delete the property
1191    [properties_ removeObjectForKey:key];
1192  } else {
1193    // Be sure the property dictionary exists
1194    if (properties_ == nil) {
1195      [self setProperties:[NSMutableDictionary dictionary]];
1196    }
1197    [properties_ setObject:obj forKey:key];
1198  }
1199}
1200
1201- (id)propertyForKey:(NSString *)key {
1202  id obj = [properties_ objectForKey:key];
1203
1204  // Be sure the returned pointer has the life of the autorelease pool,
1205  // in case self is released immediately
1206  return [[obj retain] autorelease];
1207}
1208
1209#pragma mark Utility Routines
1210
1211+ (NSString *)encodedOAuthValueForString:(NSString *)str {
1212  CFStringRef originalString = (CFStringRef) str;
1213  CFStringRef leaveUnescaped = NULL;
1214  CFStringRef forceEscaped =  CFSTR("!*'();:@&=+$,/?%#[]");
1215
1216  CFStringRef escapedStr = NULL;
1217  if (str) {
1218    escapedStr = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
1219                                                         originalString,
1220                                                         leaveUnescaped,
1221                                                         forceEscaped,
1222                                                         kCFStringEncodingUTF8);
1223    [(id)CFMakeCollectable(escapedStr) autorelease];
1224  }
1225
1226  return (NSString *)escapedStr;
1227}
1228
1229+ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict {
1230  // Make a string like "cat=fluffy@dog=spot"
1231  NSMutableString *result = [NSMutableString string];
1232  NSArray *sortedKeys = [[dict allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
1233  NSString *joiner = @"";
1234  for (NSString *key in sortedKeys) {
1235    NSString *value = [dict objectForKey:key];
1236    NSString *encodedValue = [self encodedOAuthValueForString:value];
1237    NSString *encodedKey = [self encodedOAuthValueForString:key];
1238    [result appendFormat:@"%@%@=%@", joiner, encodedKey, encodedValue];
1239    joiner = @"&";
1240  }
1241  return result;
1242}
1243
1244+ (void)invokeDelegate:(id)delegate
1245              selector:(SEL)sel
1246                object:(id)obj1
1247                object:(id)obj2
1248                object:(id)obj3 {
1249  if (delegate && sel) {
1250    NSMethodSignature *sig = [delegate methodSignatureForSelector:sel];
1251    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
1252    [invocation setSelector:sel];
1253    [invocation setTarget:delegate];
1254    [invocation setArgument:&obj1 atIndex:2];
1255    [invocation setArgument:&obj2 atIndex:3];
1256    [invocation setArgument:&obj3 atIndex:4];
1257    [invocation invoke];
1258  }
1259}
1260
1261+ (NSString *)unencodedOAuthParameterForString:(NSString *)str {
1262  NSString *plainStr = [str stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
1263  return plainStr;
1264}
1265
1266+ (NSDictionary *)dictionaryWithResponseString:(NSString *)responseStr {
1267  // Build a dictionary from a response string of the form
1268  // "cat=fluffy&dog=spot".  Missing or empty values are considered
1269  // empty strings; keys and values are percent-decoded.
1270  if (responseStr == nil) return nil;
1271
1272  NSArray *items = [responseStr componentsSeparatedByString:@"&"];
1273
1274  NSMutableDictionary *responseDict = [NSMutableDictionary dictionaryWithCapacity:[items count]];
1275
1276  for (NSString *item in items) {
1277    NSString *key = nil;
1278    NSString *value = @"";
1279
1280    NSRange equalsRange = [item rangeOfString:@"="];
1281    if (equalsRange.location != NSNotFound) {
1282      // The parameter has at least one '='
1283      key = [item substringToIndex:equalsRange.location];
1284
1285      // There are characters after the '='
1286      value = [item substringFromIndex:(equalsRange.location + 1)];
1287    } else {
1288      // The parameter has no '='
1289      key = item;
1290    }
1291
1292    NSString *plainKey = [[self class] unencodedOAuthParameterForString:key];
1293    NSString *plainValue = [[self class] unencodedOAuthParameterForString:value];
1294
1295    [responseDict setObject:plainValue forKey:plainKey];
1296  }
1297
1298  return responseDict;
1299}
1300
1301+ (NSDictionary *)dictionaryWithResponseData:(NSData *)data {
1302  NSString *responseStr = [[[NSString alloc] initWithData:data
1303                                                 encoding:NSUTF8StringEncoding] autorelease];
1304  NSDictionary *dict = [self dictionaryWithResponseString:responseStr];
1305  return dict;
1306}
1307
1308+ (NSString *)scopeWithStrings:(NSString *)str, ... {
1309  // concatenate the strings, joined by a single space
1310  NSString *result = @"";
1311  NSString *joiner = @"";
1312  if (str) {
1313    va_list argList;
1314    va_start(argList, str);
1315    while (str) {
1316      result = [result stringByAppendingFormat:@"%@%@", joiner, str];
1317      joiner = @" ";
1318      str = va_arg(argList, id);
1319    }
1320    va_end(argList);
1321  }
1322  return result;
1323}
1324
1325@end
1326
1327#endif // GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES