NewsBlur/clients/ios/Other Sources/PocketSDK/PocketAPIOperation.m

428 lines
No EOL
15 KiB
Objective-C
Executable file

//
// PocketAPIOperation.m
// PocketSDK
//
// Created by Steve Streza on 5/29/12.
// Copyright (c) 2012 Read It Later, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify,
// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
#import "PocketAPIOperation.h"
NSString *PocketAPINameForHTTPMethod(PocketAPIHTTPMethod method){
switch (method) {
case PocketAPIHTTPMethodPOST:
return @"POST";
break;
case PocketAPIHTTPMethodPUT:
return @"PUT";
break;
case PocketAPIHTTPMethodDELETE:
return @"DELETE";
break;
case PocketAPIHTTPMethodGET:
default:
return @"GET";
break;
}
}
@interface PocketAPI ()
-(void)pkt_loggedInWithUsername:(NSString *)username token:(NSString *)accessToken;
-(NSString *)pkt_userAgent;
-(NSString *)pkt_getToken;
@end
@interface PocketAPIOperation ()
-(void)pkt_connectionFinishedLoading;
-(NSMutableURLRequest *)pkt_URLRequest;
@end
@implementation PocketAPIOperation
@synthesize API, delegate, error;
@synthesize domain, HTTPMethod, APIMethod, arguments;
@synthesize connection, response, data;
-(void)start{
finishedLoading = NO;
// if there is no access token and this is not an auth method, fail and login
if(!self.API.loggedIn && !([APIMethod isEqualToString:@"request"] || [APIMethod isEqualToString:@"authorize"] || [APIMethod isEqualToString:@"oauth/authorize"])){
[self connectionFinishedWithError:[NSError errorWithDomain:(NSString *)PocketAPIErrorDomain code:401 userInfo:nil]];
return;
}
NSURLRequest *request = [self pkt_URLRequest];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
}
-(BOOL)isConcurrent{
return YES;
}
-(BOOL)isExecuting{
return !finishedLoading;
}
-(BOOL)isFinished{
return finishedLoading;
}
-(id)init{
if(self = [super init]){
domain = PocketAPIDomainDefault;
}
return self;
}
-(void)dealloc{
[API release], API = nil;
delegate = nil;
[APIMethod release], APIMethod = nil;
[arguments release], arguments = nil;
[connection release], connection = nil;
[response release], response = nil;
[data release], data = nil;
[error release], error = nil;
[super dealloc];
}
-(NSString *)description{
return [NSString stringWithFormat:@"<%@: %p https://%@%@ %@>", [self class], self, self.baseURLPath, self.APIMethod, self.arguments];
}
-(NSString *)baseURLPath{
switch (self.domain) {
case PocketAPIDomainAuth:
return @"getpocket.com/v3/oauth";
break;
case PocketAPIDomainDefault:
default:
return @"getpocket.com/v3";
break;
}
}
-(NSDictionary *)responseDictionary{
NSString *contentType = [[self.response allHeaderFields] objectForKey:@"Content-Type"];
if([contentType isEqualToString:@"application/json"]){
Class nsJSONSerialization = NSClassFromString(@"NSJSONSerialization");
return [nsJSONSerialization JSONObjectWithData:self.data options:0 error:nil];
}else if([contentType rangeOfString:@"application/x-www-form-urlencode"].location != NSNotFound){
NSString *formString = [[[NSString alloc] initWithData:self.data encoding:NSUTF8StringEncoding] autorelease];
return [NSDictionary pkt_dictionaryByParsingURLEncodedFormString:formString];
}else{
return nil;
}
}
#pragma mark NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)aConnection didReceiveResponse:(NSURLResponse *)receivedResponse{
response = (NSHTTPURLResponse *)[receivedResponse retain];
if([response statusCode] == 200){
data = [[NSMutableData alloc] initWithCapacity:0];
}else if([[response allHeaderFields] objectForKey:@"X-Error"]){
[connection cancel];
NSString *xError = [[response allHeaderFields] objectForKey:@"X-Error"];
NSDictionary *userInfo = xError ? [NSDictionary dictionaryWithObjectsAndKeys:xError,NSLocalizedDescriptionKey,nil] : nil;
[self connection:connection didFailWithError:[NSError errorWithDomain:@"PocketSDK"
code:[response statusCode]
userInfo:userInfo]];
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)inData{
[data appendData:inData];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)theError{
[self connectionFinishedWithError:theError];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
[self connectionFinishedWithError:nil];
}
-(void)connectionFinishedWithError:(NSError *)theError{
NSInteger statusCode = (self.response ? self.response.statusCode : theError.code);
BOOL needsToRelogin = !attemptedRelogin && statusCode == 401;
BOOL needsToLogout = statusCode == 403;
BOOL serverError = statusCode >= 500;
NSInteger errorCode = [[self.response.allHeaderFields objectForKey:@"X-Error-Code"] intValue];
NSString *errorDescription = [self.response.allHeaderFields objectForKey:@"X-Error"];
if(serverError){
errorCode = PocketAPIErrorServerMaintenance;
errorDescription = @"There was a server error.";
}
NSError *pocketError = nil;
if(errorCode){
pocketError = [NSError errorWithDomain:(NSString *)PocketAPIErrorDomain
code:errorCode
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
errorDescription, @"localizedDescription",
theError, @"HTTPError",
nil]];
}else if(theError){
pocketError = [NSError errorWithDomain:(NSString *)PocketAPIErrorDomain
code:statusCode
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
errorDescription, @"localizedDescription",
theError, @"HTTPError",
nil]];
}else if(needsToLogout){
pocketError = [NSError errorWithDomain:(NSString *)PocketAPIErrorDomain code:statusCode userInfo:nil];
}
error = [pocketError retain];
if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPI:receivedResponse:forAPIMethod:error:)]){
[self.delegate pocketAPI:self.API receivedResponse:[self responseDictionary] forAPIMethod:self.APIMethod error:theError];
}
// if the user has deauthorized the app, we bounce them to the Pocket to re-login
// if this succeeds, we re-call the API the app requested
// if it fails, then prompt for an error next time
if(needsToRelogin){
[self.API loginWithDelegate:self];
attemptedRelogin = YES;
return;
}
if(needsToLogout){
[self.API logout];
}
if(error){
if([self.APIMethod rangeOfString:@"auth"].location != NSNotFound || [self.APIMethod isEqualToString:@"request"]){
if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPI:hadLoginError:)]){
[self.delegate pocketAPI:self.API hadLoginError:error];
}
}else if([self.APIMethod isEqualToString:@"add"]){
if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPI:failedToSaveURL:error:)]){
[self.delegate pocketAPI:self.API
failedToSaveURL:[NSURL URLWithString:[self.arguments objectForKey:@"url"]]
error:error];
}
}
}else{
if([self.APIMethod isEqualToString:@"auth"]){
[self.API pkt_loggedInWithUsername:[self.arguments objectForKey:@"username"] token:[self.arguments objectForKey:@"token"]];
if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPILoggedIn:)]){
[self.delegate pocketAPILoggedIn:self.API];
}
}else if([self.APIMethod isEqualToString:@"add"]){
if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPI:savedURL:)]){
NSString *urlString = [self.arguments objectForKey:@"url"];
NSURL *url = urlString ? [NSURL URLWithString:urlString] : nil;
[self.delegate pocketAPI:self.API
savedURL:url];
}
}
else if([self.APIMethod isEqualToString:@"request"]){
NSDictionary *responseDict = [self responseDictionary];
[self.delegate pocketAPI:self.API receivedRequestToken:[responseDict objectForKey:@"code"]];
}
else if([self.APIMethod isEqualToString:@"authorize"] || [self.APIMethod isEqualToString:@"oauth/authorize"]){
NSDictionary *responseDict = [self responseDictionary];
NSString *username = [responseDict objectForKey:@"username"];
if((!username || username == (id)[NSNull null]) && [[[self arguments] objectForKey:@"grant_type"] isEqualToString:@"credentials"]){
username = [[self arguments] objectForKey:@"username"];
}
NSString *token = [responseDict objectForKey:@"access_token"];
if((id)username == [NSNull null] && (id)token == [NSNull null]){
[self.delegate pocketAPI:self.API hadLoginError:[NSError errorWithDomain:@"PocketAPI" code:404 userInfo:nil]];
}else{
[self.API pkt_loggedInWithUsername:username token:token];
if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPILoggedIn:)]){
[self.delegate pocketAPILoggedIn:self.API];
}
}
}
}
[self pkt_connectionFinishedLoading];
}
#pragma mark Handling Re-login
-(void)pocketAPILoggedIn:(PocketAPI *)api{
[[self.API operationQueue] addOperation:[[self copy] autorelease]];
}
-(void)pocketAPI:(PocketAPI *)api hadLoginError:(NSError *)theError{
[self connectionFinishedWithError:theError];
}
#pragma mark Private APIs
-(NSDictionary *)pkt_requestArguments{
NSMutableDictionary *dict = [[self.arguments mutableCopy] autorelease];
if(self.API.consumerKey){
[dict setObject:self.API.consumerKey forKey:@"consumer_key"];
}
NSString *accessToken = [self.API pkt_getToken];
if(accessToken){
[dict setObject:accessToken forKey:@"access_token"];
}
return dict;
}
-(NSMutableURLRequest *)pkt_URLRequest{
NSString *urlString = [NSString stringWithFormat:@"https://%@/%@", self.baseURLPath, self.APIMethod];
NSDictionary *requestArgs = [self pkt_requestArguments];
if(self.HTTPMethod == PocketAPIHTTPMethodGET && requestArgs.count > 0){
NSMutableArray *pairs = [NSMutableArray array];
for(NSString *key in [requestArgs allKeys]){
[pairs addObject:[NSString stringWithFormat:@"%@=%@",key, [PocketAPIOperation encodeForURL:[requestArgs objectForKey:key]]]];
}
if(pairs.count > 0){
urlString = [urlString stringByAppendingFormat:@"?%@", [pairs componentsJoinedByString:@"&"]];
}
}
NSURL *url = [NSURL URLWithString:urlString];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:PocketAPINameForHTTPMethod(self.HTTPMethod)];
[request setTimeoutInterval:20.];
[request setCachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData];
Class nsJSONSerialization = NSClassFromString(@"NSJSONSerialization");
if(self.HTTPMethod != PocketAPIHTTPMethodGET && requestArgs.count > 0){
if(nsJSONSerialization != nil){
[request addValue:@"application/json; charset=UTF-8" forHTTPHeaderField:@"Content-Type"];
[request setHTTPBody:[nsJSONSerialization dataWithJSONObject:requestArgs options:0 error:nil]];
}else{
[request addValue:@"application/x-www-form-urlencoded; charset=UTF-8" forHTTPHeaderField:@"Content-Type"];
[request setHTTPBody:[[requestArgs pkt_URLEncodedFormString] dataUsingEncoding:NSUTF8StringEncoding]];
}
}
NSString *userAgent = [self.API pkt_userAgent];
if(userAgent){
[request addValue:userAgent forHTTPHeaderField:@"User-Agent"];
}
if(nsJSONSerialization != nil){
[request addValue:@"application/json" forHTTPHeaderField:@"X-Accept"];
}else{
[request addValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"X-Accept"];
}
return [request autorelease];
}
+(NSString *)encodeForURL:(NSString *)urlStr
{
NSString *result = (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
(CFStringRef)urlStr,
NULL,
CFSTR("!*'();:@&=+$,/?%#[]"),
kCFStringEncodingUTF8);
return [result autorelease];
}
+(NSString *)decodeForURL:(NSString *)urlStr{
NSString *result = (NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault,
(CFStringRef)urlStr,
CFSTR(""),
kCFStringEncodingUTF8);
return [result autorelease];
}
-(void)pkt_connectionFinishedLoading{
[self willChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
finishedLoading = YES;
[self didChangeValueForKey:@"isFinished"];
[self didChangeValueForKey:@"isExecuting"];
[delegate release], delegate = nil;
}
+(NSError *)errorFromXError:(NSString *)xError
withErrorCode:(NSUInteger)errorCode
HTTPStatusCode:(NSUInteger)statusCode{
return nil;
}
#pragma mark NSCopying
- (id)copyWithZone:(NSZone *)zone{
PocketAPIOperation *operation = [[PocketAPIOperation allocWithZone:zone] init];
operation.API = self.API;
operation.delegate = self.delegate;
operation.domain = self.domain;
operation.HTTPMethod = self.HTTPMethod;
operation.APIMethod = self.APIMethod;
operation.arguments = self.arguments;
return operation;
}
@end
@implementation NSDictionary (PocketAdditions)
-(NSString *)pkt_URLEncodedFormString{
NSMutableArray *formPieces = [NSMutableArray arrayWithCapacity:self.allKeys.count];
for(NSString *key in self.allKeys){
NSString *value = [self objectForKey:key];
[formPieces addObject:[NSString stringWithFormat:@"%@=%@", [PocketAPIOperation encodeForURL:key], [PocketAPIOperation encodeForURL:value]]];
}
return [formPieces componentsJoinedByString:@"&"];
}
+(NSDictionary *)pkt_dictionaryByParsingURLEncodedFormString:(NSString *)formString{
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
NSArray *formPieces = [formString componentsSeparatedByString:@"&"];
for(NSString *formPiece in formPieces){
NSArray *fieldPieces = [formPiece componentsSeparatedByString:@"="];
if(fieldPieces.count == 2){
NSString *fieldKey = [fieldPieces objectAtIndex:0];
NSString *fieldValue = [fieldPieces objectAtIndex:1];
[dictionary setObject:[PocketAPIOperation decodeForURL:fieldValue]
forKey:[PocketAPIOperation decodeForURL:fieldKey]];
}
}
return [[dictionary copy] autorelease];
}
@end