Plugins/Displays/SMS/GrowlSMSDisplay.m
author boredzo
Sun Jul 06 17:31:40 2008 +0000 (2008-07-06)
changeset 4141 494e5044075d
parent 3706 3fe9b5224f0c
child 4143 f673f14da226
permissions -rw-r--r--
Fixed some memory leaks reported by the clang static analyzer, by autoreleasing these objects immediately rather than releasing some of them later (and not all of the time).
     1 //
     2 //  GrowlSMSDisplay.m
     3 //  Growl Display Plugins
     4 //
     5 //  Created by Diggory Laycock
     6 //  Copyright 2005-2006 The Growl Project All rights reserved.
     7 //
     8 #import "GrowlSMSDisplay.h"
     9 #import "GrowlSMSPrefs.h"
    10 #import "NSStringAdditions.h"
    11 #import "GrowlDefinesInternal.h"
    12 #import "GrowlApplicationNotification.h"
    13 #include <Security/SecKeychain.h>
    14 #include <Security/SecKeychainItem.h>
    15 
    16 #define keychainServiceName "GrowlSMS"
    17 #define keychainAccountName "SMSWebServicePassword"
    18 
    19 #define GrowlSMSPrefDomain		@"com.Growl.SMS"
    20 #define accountNameKey			@"SMS - Account Name"
    21 #define accountAPIIDKey			@"SMS - Account API ID"
    22 #define destinationNumberKey	@"SMS - Destination Number"
    23 
    24 
    25 @implementation GrowlSMSDisplay
    26 
    27 - (id) init {
    28 	if ((self = [super init])) {
    29 		commandQueue = [[NSMutableArray alloc] init];
    30 		xmlHoldingStringValue = [[NSMutableString alloc] init];
    31 		waitingForResponse = NO;
    32 		creditBalance = 0.0f;
    33 	}
    34 	return self;
    35 }
    36 
    37 - (void) dealloc {
    38 	[commandQueue release];
    39 
    40 	[preferencePane release];
    41 	[super dealloc];
    42 }
    43 
    44 - (NSPreferencePane *) preferencePane {
    45 	if (!preferencePane)
    46 		preferencePane = [[GrowlSMSPrefs alloc] initWithBundle:[NSBundle bundleWithIdentifier:@"com.Growl.SMS"]];
    47 	return preferencePane;
    48 }
    49 
    50 - (void) displayNotification:(GrowlApplicationNotification *)notification {
    51 	NSString	*accountNameValue = nil;
    52 	NSString	*apiIDValue = nil;
    53 	NSString	*destinationNumberValue = nil;
    54 
    55 	READ_GROWL_PREF_VALUE(destinationNumberKey, GrowlSMSPrefDomain, NSString *, &destinationNumberValue);
    56 	[destinationNumberValue autorelease];
    57 	READ_GROWL_PREF_VALUE(accountAPIIDKey, GrowlSMSPrefDomain, NSString *, &apiIDValue);
    58 	[apiIDValue autorelease];
    59 	READ_GROWL_PREF_VALUE(accountNameKey, GrowlSMSPrefDomain, NSString *, &accountNameValue);
    60 	[accountNameValue autorelease];
    61 
    62 	if (!([destinationNumberValue length] && [apiIDValue length] && [accountNameValue length])) {
    63 		NSLog(@"Cannot send SMS - not enough details in preferences.");
    64 		return;
    65 	}
    66 
    67 	NSDictionary *noteDict = [notification dictionaryRepresentation];
    68 	NSString *title = [noteDict objectForKey:GROWL_NOTIFICATION_TITLE];
    69 	NSString *desc = [noteDict objectForKey:GROWL_NOTIFICATION_DESCRIPTION];
    70 
    71 	//	Fetch the SMS password from the keychain
    72 	unsigned char *password;
    73 	UInt32 passwordLength;
    74 	OSStatus status;
    75 	status = SecKeychainFindGenericPassword(NULL,
    76 											strlen(keychainServiceName), keychainServiceName,
    77 											strlen(keychainAccountName), keychainAccountName,
    78 											&passwordLength, (void **)&password, NULL);
    79 
    80 	CFStringRef passwordString;
    81 	if (status == noErr) {
    82 		passwordString = CFStringCreateWithBytes(kCFAllocatorDefault, password, passwordLength, kCFStringEncodingUTF8, false);
    83 		SecKeychainItemFreeContent(NULL, password);
    84 	} else {
    85 		if (status != errSecItemNotFound)
    86 			NSLog(@"Failed to retrieve SMS Account password from keychain. Error: %d", status);
    87 		passwordString = CFSTR("");
    88 	}
    89 
    90 
    91 	NSString *localHostName = [[NSHost currentHost] name];
    92 	NSString *smsSendCommand = [[NSString alloc] initWithFormat:
    93 		@"<clickAPI><sendMsg><api_id>%@</api_id><user>%@</user><password>%@</password><to>+%@</to><text>(%@) %@ (%@)</text><from>Growl</from></sendMsg></clickAPI>",
    94 		apiIDValue,
    95 		accountNameValue,
    96 		passwordString,
    97 		destinationNumberValue,
    98 		title,
    99 		desc,
   100 		localHostName];
   101 
   102 	NSLog(@"SMS Display...  %@" , smsSendCommand);
   103 	[self sendXMLCommand:smsSendCommand];
   104 	[smsSendCommand release];
   105 
   106 	//	Check credit balance.
   107 	NSString *checkBalanceCommand = [[NSString alloc] initWithFormat:
   108 		@"<clickAPI><getBalance><api_id>%@</api_id><user>%@</user><password>%@</password></getBalance></clickAPI>",
   109 		apiIDValue,
   110 		accountNameValue,
   111 		passwordString];
   112 
   113 	CFRelease(passwordString);
   114 
   115 	[self sendXMLCommand:checkBalanceCommand];
   116 	[checkBalanceCommand release];
   117 
   118 	id clickContext = [noteDict objectForKey:GROWL_NOTIFICATION_CLICK_CONTEXT];
   119 	if (clickContext) {
   120 		NSDictionary *userInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
   121 			[noteDict objectForKey:@"ClickHandlerEnabled"], @"ClickHandlerEnabled",
   122 			clickContext,                                   GROWL_KEY_CLICKED_CONTEXT,
   123 			[noteDict objectForKey:GROWL_APP_PID],          GROWL_APP_PID,
   124 			nil];
   125 		[[NSNotificationCenter defaultCenter] postNotificationName:GROWL_NOTIFICATION_TIMED_OUT
   126 															object:[notification applicationName]
   127 														  userInfo:userInfo];
   128 		[userInfo release];
   129 	}
   130 
   131 }
   132 
   133 
   134 #pragma mark -
   135 #pragma mark Accessors
   136 
   137 - (NSData *) responseData {
   138 	return responseData;
   139 }
   140 
   141 - (void) setResponseData:(NSData *)newResponseData {
   142 	[newResponseData retain];
   143 	[responseData release];
   144 	responseData = newResponseData;
   145 
   146 	NSLog(@"responseData:  %@", responseData);
   147 }
   148 
   149 
   150 #pragma mark -
   151 #pragma mark Instance Methods
   152 
   153 
   154 /*
   155  <clickAPI>
   156 	 <sendMsg>
   157 		 <api_id>your_api_id</api_id>
   158 		 <user>your_user_name</user>
   159 		 <password>your_pass</password>
   160 		 <to>+12343455667</to>
   161 		 <text>Test text message.</text>
   162 		 <from>Growl</from>
   163 	 </sendMsg>
   164  </clickAPI>
   165 
   166 
   167  API URL:
   168  ==========
   169  https://api.clickatell.com/xml/xml
   170  <input name="data" type="text" value="<clickAPI>$your_xml_data</clickAPI>">
   171 
   172  //	To do - use the unicode option - when needed - although, it halves the length of SMS we can send.
   173 
   174  */
   175 - (void) sendXMLCommand:(NSString *)commandString {
   176 	CFStringRef			dataString = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("data=%@"), commandString);
   177 	CFDataRef			postData = CFStringCreateExternalRepresentation(kCFAllocatorDefault, dataString, kCFStringEncodingUTF8, 0U);
   178 	CFURLRef			clickatellURL = CFURLCreateWithString(kCFAllocatorDefault, CFSTR("https://api.clickatell.com/xml/xml"), NULL);
   179 	NSMutableURLRequest	*post = [[NSMutableURLRequest alloc] initWithURL:(NSURL *)clickatellURL];
   180 	CFStringRef			contentLength = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%u"), CFDataGetLength(postData));
   181 
   182 	NSLog(@"Sending data: %@", postData);
   183 
   184 	[post addValue:(NSString *)contentLength forHTTPHeaderField: @"Content-Length"];
   185 	[post setHTTPMethod:@"POST"];
   186 	[post setHTTPBody:(NSData *)postData];
   187 	[commandQueue addObject:post];
   188 	[post release];
   189 
   190 	CFRelease(postData);
   191 	CFRelease(dataString);
   192 	CFRelease(clickatellURL);
   193 	CFRelease(contentLength);
   194 
   195 	[self processQueue];
   196 }
   197 
   198 
   199 - (void) processQueue {
   200 	// NSLog(@"Processing HTTP Command Queue");
   201 	if (![commandQueue count]) {
   202 		// NSLog(@"Queue is empty...");
   203 		return;
   204 	}
   205 
   206 	if (!waitingForResponse) {
   207 		waitingForResponse = YES;
   208 		NSLog(@"Beginning Command Request Connection...");
   209 		[NSURLConnection connectionWithRequest:[commandQueue objectAtIndex:0U] delegate: self];
   210 	} else {
   211 		NSLog(@"Can't process queue  - we are waiting on an existing command's resonse..");
   212 	}
   213 }
   214 
   215 
   216 - (void) connectionDidRespond {
   217 	NSLog(@"Request/Response transaction complete...");
   218 	waitingForResponse = NO;
   219 	[commandQueue removeObjectAtIndex:0U];
   220 	[self processQueue];
   221 }
   222 
   223 - (void) handleResponse {
   224 	if (responseParser)
   225 		[responseParser release];
   226 	responseParser = [[NSXMLParser alloc] initWithData:[self responseData]];
   227 	[responseParser setDelegate:self];
   228 	[responseParser setShouldResolveExternalEntities:YES];
   229 	[responseParser parse]; // return value not used
   230 							// if not successful, delegate is informed of error}
   231 }
   232 
   233 
   234 #pragma mark -
   235 #pragma mark NSXMLParser Delegate methods:
   236 
   237 - (void) parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
   238 #pragma unused(parser,namespaceURI,qName,attributeDict)
   239 	if ([elementName isEqualToString:@"clickAPI"]) {
   240 //		NSLog(@"Found the clickAPI element in the response.  That means we got the HTTP part right.");
   241 	} else if ([elementName isEqualToString:@"xmlErrorResp"]) {
   242 		NSLog(@"Oh Noes! we got an error back from clickatell - we passed them a bad XML request...");
   243 	} else if ([elementName isEqualToString:@"fault"]) {
   244 //		NSLog(@"Here comes the fault:...");
   245 	} else if ([elementName isEqualToString:@"getBalanceResp"]) {
   246 //		NSLog(@"Here comes the Balance response:...");
   247 		inBalanceResponseElement = YES;
   248 	} else if ([elementName isEqualToString:@"ok"]) {
   249 //		NSLog(@"Command Success.");
   250 		if (inBalanceResponseElement) {
   251 //			NSLog(@"Here comes the Balance value:...");
   252 		}
   253 	} else if ([elementName isEqualToString:@"sendMsgResp"]) {
   254 //		NSLog(@"Here comes the Message Send response:...");
   255 		inMessageSendResponseElement = YES;
   256 	}
   257 }
   258 
   259 
   260 - (void) parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
   261 #pragma unused(parser)
   262 	if (!xmlHoldingStringValue)
   263 		xmlHoldingStringValue = [[NSMutableString alloc] initWithCapacity:50];
   264 	[xmlHoldingStringValue appendString:string];
   265 }
   266 
   267 
   268 - (void) parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
   269 #pragma unused(parser,namespaceURI,qName)
   270 	if (   [elementName isEqualToString:@"clickAPI"]
   271 		|| [elementName isEqualToString:@"xmlErrorResp"]) {
   272 		// nothing to do
   273 		return;
   274 	} else if ([elementName isEqualToString:@"getBalanceResp"]) {
   275 		inBalanceResponseElement = NO;
   276 		[xmlHoldingStringValue release];
   277 		xmlHoldingStringValue = nil;
   278 	} else if ([elementName isEqualToString:@"sendMsgResp"]) {
   279 		inMessageSendResponseElement = NO;
   280 		[xmlHoldingStringValue release];
   281 		xmlHoldingStringValue = nil;
   282 	} else if ([elementName isEqualToString:@"fault"]) {
   283 		NSLog(@"The fault was: %@" , [xmlHoldingStringValue stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]] );
   284 		[xmlHoldingStringValue release];
   285 		xmlHoldingStringValue = nil;
   286 	} else if ([elementName isEqualToString:@"ok"]) {
   287 		if (inBalanceResponseElement) {
   288 			creditBalance = [[xmlHoldingStringValue stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]] floatValue];
   289 			NSLog(@"Your Balance is: %4.1f 'credits'" , creditBalance);
   290 			[xmlHoldingStringValue release];
   291 			xmlHoldingStringValue = nil;
   292 		}
   293 	} else if ([elementName isEqualToString:@"apiMsgId"]) {
   294 		if (inMessageSendResponseElement) {
   295 			NSLog(@"SMS Message Sent (messageId: %@)" , [xmlHoldingStringValue stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]);
   296 			[xmlHoldingStringValue release];
   297 			xmlHoldingStringValue = nil;
   298 		}
   299 	} else {
   300 		NSLog(@"SMS display: unknown XML element: %@", elementName);
   301 	}
   302 }
   303 
   304 - (void) parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError {
   305 	NSLog(@"Error Parsing XML response from SMS Gateway - %i, Description: %@, Line: %i, Column: %i",	[parseError code],
   306 		  [[parser parserError] localizedDescription],
   307 		  [parser lineNumber],
   308 		  [parser columnNumber]);
   309 }
   310 
   311 
   312 #pragma mark -
   313 #pragma mark NSURLConnection Delegate methods:
   314 
   315 
   316 /*
   317 	The delegate receives this message if connection has cancelled the authentication challenge specified by challenge.
   318  */
   319 - (void) connection:(NSURLConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
   320 #pragma unused(connection,challenge)
   321 	NSLog(@"didCancelAuthenticationChallenge:");
   322 	[self connectionDidRespond];
   323 }
   324 
   325 
   326 /*
   327 	The delegate receives this message if connection has failed to load the request successfully. The details of the failure are specified in error.
   328 	Once the delegate receives this message, it will receive no further messages for connection.
   329  */
   330 - (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
   331 #pragma unused(connection)
   332 	NSLog(@"Connection to SMS Web API failed: (%@)", [error localizedDescription]);
   333 
   334 	[self connectionDidRespond];
   335 }
   336 
   337 
   338 /*
   339  The delegate receives this message when connection must authenticate challenge in order to download the request. This method gives the delegate the opportunity to determine the course of action taken for the challenge: provide credentials, continue without providing credentials or cancel the authentication challenge and the download.
   340  The delegate can determine the number of previous authentication challenges by sending the message previousFailureCount to challenge.
   341 
   342  If the previous failure count is 0 and the value returned by proposedCredential is nil, the delegate can create a new NSURLCredential object, providing a user name and password, and send a useCredential:forAuthenticationChallenge: message to [challenge sender], passing the credential and challenge as parameters. If proposedCredential is not nil, the value is a credential from the URL or the shared credential storage that can be provided to the user as feedback.
   343  The delegate may decide to abandon further attempts at authentication at any time by sending [challenge sender] a continueWithoutCredentialForAuthenticationChallenge: or a cancelAuthenticationChallenge: message. The specific action will be implementation dependent.
   344 
   345  If the delegate implements this method, the download will suspend until [challenge sender] is sent one of the following messages: useCredential:forAuthenticationChallenge:, continueWithoutCredentialForAuthenticationChallenge: or cancelAuthenticationChallenge:.
   346  If the delegate does not implement this method the default implementation is used. If a valid credential for the request is provided as part of the URL, or is available from the NSURLCredentialStorage the [challenge sender] is sent a useCredential:forAuthenticationChallenge: with the credential. If the challenge has no credential or the credentials fail to authorize access, then continueWithoutCredentialForAuthenticationChallenge: is sent to [challenge sender] instead.
   347 
   348  See Also: – cancelAuthenticationChallenge:, – continueWithoutCredentialForAuthenticationChallenge:, – useCredential:forAuthenticationChallenge:
   349  */
   350 - (void) connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
   351 	NSLog(@"didReceiveAuthenticationChallenge: %@", challenge);
   352 	//	It doesn't need web auth currently - so we're not going to handle this case.
   353 	[connection cancel];
   354 	//	[self connectionDidRespond];
   355 }
   356 
   357 
   358 /*
   359 	The delegate receives this message as connection loads data incrementally. The delegate should concatenate the contents of each data object delivered to build up the complete data for a URL load.
   360 	This method provides the only way for an asynchronous delegate to retrieve the loaded data. It is the responsibility of the delegate to retain or copy this data as it is delivered.
   361  */
   362 - (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
   363 #pragma unused(connection)
   364 	//	NSLog(@"didReceiveData:  %@", data);
   365 	[self setResponseData: data];
   366 	[self handleResponse];
   367 }
   368 
   369 
   370 /*
   371 	The delegate receives this message when the URL loading system has received sufficient load data for connection to construct the NSURLResponse object, response.
   372  The response is immutable and will not be modified by the URL loading system once it is presented to the delegate.
   373 
   374  In rare cases, for example in the case of a HTTP load where the content type of the load data is multipart/x-mixed-replace, the delegate will receive more than one connection:didReceiveResponse: message.
   375  In the event this occurs, delegates should discard all data previously delivered by connection:didReceiveData:, and should be prepared to handle the, potentially different, MIME type reported by the NSURLResponse.
   376 
   377  Note that the only case where this message is not sent to the delegate is when the protocol implementation encounters an error before a response could be created.
   378 
   379  */
   380 
   381 - (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
   382 #pragma unused(connection,response)
   383 //	NSLog(@"didReceiveResponse:  URL(%@) expectedDataLength:(%d)", [response URL], [response expectedContentLength]  );
   384 
   385 //	NSLog(@" MIME:(%@)" , [response MIMEType]);
   386 //	NSLog(@" textEncoding:(%@)" , [response textEncodingName]);
   387 }
   388 
   389 
   390 - (NSCachedURLResponse *) connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
   391 #pragma unused(connection,cachedResponse)
   392 	//	No Caching please...  Since we're using HTTPS none should occur - but no harm in being cautious.
   393 	return nil;
   394 }
   395 
   396 
   397 /*
   398 
   399  The delegate receives this message when connection determines that it must change URLs in order to continue loading a request.
   400  The delegate should inspect the redirected request specified by request and copy and modify request as necessary to change its attributes, or return request unmodified.
   401  The NSURLResponse that caused the redirect is specified by redirectResponse.
   402  The redirectResponse will be nil in cases where this method is not being sent as a result of involving the delegate in redirect processing.
   403  If the delegate wishes to cancel the redirect, it should call the connection object’s cancel method.
   404  Alternatively, the delegate method can return nil to cancel the redirect, and the connection will continue to process.
   405  This has special relevance in the case where redirectResponse is not nil.
   406  In this case, any data that is loaded for the connection will be sent to the delegate, and the delegate will receive a connectionDidFinishLoading or connection:didFailLoadingWithError: message, as appropriate.
   407 
   408  The delegate can receive this message as a result of transforming a request’s URL to its canonical form, or for protocol-specific reasons, such as an HTTP redirect.
   409  The delegate implementation should be prepared to receive this message multiple times.
   410 
   411  */
   412 
   413 - (NSURLRequest *) connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse {
   414 #pragma unused(request,redirectResponse)
   415 	NSLog(@"redirectResponse:");
   416 	[connection cancel];
   417 	return nil;
   418 }
   419 
   420 
   421 // This delegate method is called when connection has finished loading successfully. The delegate will receive no further messages for connection.
   422 - (void) connectionDidFinishLoading:(NSURLConnection *)connection {
   423 #pragma unused(connection)
   424 	// NSLog(@"connectionDidFinishLoading:");
   425 	[self connectionDidRespond];
   426 }
   427 
   428 
   429 - (BOOL) requiresPositioning {
   430 	return NO;
   431 }
   432 
   433 @end