Source/AIXMLChatlogConverter.m
author Stephen Holt <sholt@adium.im>
Wed Aug 26 13:17:35 2009 -0400 (2009-08-26)
changeset 2597 07e07f9788a6
parent 1946 866f1f27b315
child 2799 ba94093fbed8
permissions -rw-r--r--
Undo the part of [3855f70905bd]: It caused strings to be incorrectly escaped and causing our XML parsing to break. Rather, escape hash-symbols when parsing in XML data as not to confuse AIHTMLDecoder. Refs #8141. Fixes #12856.
     1 /*
     2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
     3  * with this source distribution.
     4  * 
     5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
     6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
     7  * or (at your option) any later version.
     8  * 
     9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
    10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
    11  * Public License for more details.
    12  * 
    13  * You should have received a copy of the GNU General Public License along with this program; if not,
    14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
    15  */
    16 
    17 #import "AIXMLChatlogConverter.h"
    18 #import "AIStandardListWindowController.h"
    19 #import <Adium/AIHTMLDecoder.h>
    20 #import <Adium/AIListContact.h>
    21 #import <Adium/AIService.h>
    22 #import <Adium/AIAccountControllerProtocol.h>
    23 #import <Adium/AIContactControllerProtocol.h>
    24 #import <Adium/AIContentControllerProtocol.h>
    25 #import <Adium/AIStatusControllerProtocol.h>
    26 #import <AIUtilities/NSCalendarDate+ISO8601Parsing.h>
    27 #import <AIUtilities/AIDateFormatterAdditions.h>
    28 #import <AIUtilities/AIStringAdditions.h>
    29 
    30 #define PREF_GROUP_WEBKIT_MESSAGE_DISPLAY		@"WebKit Message Display"
    31 #define KEY_WEBKIT_USE_NAME_FORMAT				@"Use Custom Name Format"
    32 #define KEY_WEBKIT_NAME_FORMAT					@"Name Format"
    33 
    34 static void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context);
    35 static void addChild(CFXMLParserRef parser, void *parent, void *child, void *context);
    36 static void endStructure(CFXMLParserRef parser, void *xmlType, void *context);
    37 
    38 @implementation AIXMLChatlogConverter
    39 
    40 + (NSAttributedString *)readFile:(NSString *)filePath withOptions:(NSDictionary *)options
    41 {
    42 	AIXMLChatlogConverter *converter = [[AIXMLChatlogConverter alloc] init];
    43 	NSAttributedString *ret = [[converter readFile:filePath withOptions:options] retain];
    44 	[converter release];
    45 	return [ret autorelease];
    46 }
    47 
    48 - (id)init
    49 {
    50 	if ((self = [super init])) {
    51 	
    52 		state = XML_STATE_NONE;
    53 		
    54 		inputFileString = nil;
    55 		sender = nil;
    56 		mySN = nil;
    57 		myDisplayName = nil;
    58 		date = nil;
    59 		parser = NULL;
    60 		status = nil;
    61 		
    62 		dateFormatter = [[NSDateFormatter localizedDateFormatterShowingSeconds:YES showingAMorPM:YES] retain];
    63 		
    64 		newlineAttributedString = [[NSAttributedString alloc] initWithString:@"\n" attributes:nil];
    65 
    66 		statusLookup = [[NSDictionary alloc] initWithObjectsAndKeys:
    67 			AILocalizedString(@"Online", nil), @"online",
    68 			AILocalizedString(@"Idle", nil), @"idle",
    69 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_OFFLINE], @"offline",
    70 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_AWAY], @"away",
    71 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_AVAILABLE], @"available",
    72 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_BUSY], @"busy",
    73 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AT_HOME], @"notAtHome",
    74 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_PHONE], @"onThePhone",
    75 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_VACATION], @"onVacation",
    76 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_DND], @"doNotDisturb",
    77 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_EXTENDED_AWAY], @"extendedAway",
    78 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_BRB], @"beRightBack",
    79 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AVAILABLE], @"notAvailable",
    80 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AT_DESK], @"notAtMyDesk",
    81 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_IN_OFFICE], @"notInTheOffice",
    82 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_STEPPED_OUT], @"steppedOut",
    83 			nil];
    84 			
    85 		if ([[adium.preferenceController preferenceForKey:KEY_WEBKIT_USE_NAME_FORMAT
    86 													  group:PREF_GROUP_WEBKIT_MESSAGE_DISPLAY] boolValue]) {
    87 			nameFormat = [[adium.preferenceController preferenceForKey:KEY_WEBKIT_NAME_FORMAT
    88 																   group:PREF_GROUP_WEBKIT_MESSAGE_DISPLAY] integerValue];
    89 		} else {
    90 			nameFormat = AIDefaultName;
    91 		}
    92 	}
    93 
    94 	return self;
    95 }
    96 
    97 - (void)dealloc
    98 {
    99 	[dateFormatter release];
   100 	[newlineAttributedString release];
   101 	[inputFileString release];
   102 	[eventTranslate release];
   103 	[sender release];
   104 	[senderAlias release];
   105 	[mySN release];
   106 	[myDisplayName release];
   107 	[service release];
   108 	[date release];
   109 	[status release];
   110 	[output release];
   111 	[statusLookup release];
   112 	[htmlDecoder release];
   113 	[super dealloc];
   114 }
   115 
   116 - (NSAttributedString *)readFile:(NSString *)filePath withOptions:(NSDictionary *)options
   117 {
   118 	NSData *inputData = [NSData dataWithContentsOfFile:filePath]; 
   119 	inputFileString = [[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding]; 
   120 	NSURL *url = [[NSURL alloc] initFileURLWithPath:filePath];
   121 	output = [[NSMutableAttributedString alloc] init];
   122 	
   123 	htmlDecoder = [[AIHTMLDecoder alloc] init];
   124 	[htmlDecoder setBaseURL:[filePath stringByDeletingLastPathComponent]];
   125 	
   126 	showTimestamps = [[options objectForKey:@"showTimestamps"] boolValue];
   127 	showEmoticons = [[options objectForKey:@"showEmoticons"] boolValue];
   128 
   129 	CFXMLParserCallBacks callbacks = {
   130 		0,
   131 		createStructure,
   132 		addChild,
   133 		endStructure,
   134 		NULL,
   135 		NULL
   136 	};
   137 	CFXMLParserContext context = {
   138 		0,
   139 		self,
   140 		CFRetain,
   141 		CFRelease,
   142 		NULL
   143 	};
   144 	parser = CFXMLParserCreate(NULL, (CFDataRef)inputData, NULL, kCFXMLParserSkipMetaData | kCFXMLParserSkipWhitespace, kCFXMLNodeCurrentVersion, &callbacks, &context);
   145 	if (!CFXMLParserParse(parser)) {
   146 		NSLog(@"%@: Parser %@ for inputFileString %@ returned false.",
   147 			  [self class], parser, inputFileString);
   148 		[output release];
   149 		output = nil;
   150 	}
   151 	CFRelease(parser);
   152 	parser = nil;
   153 	[url release];
   154 	return output;
   155 }
   156 
   157 - (void)startedElement:(NSString *)name info:(const CFXMLElementInfo *)info
   158 {
   159 	NSDictionary *attributes = (NSDictionary *)info->attributes;
   160 	
   161 	switch(state){
   162 		case XML_STATE_NONE:
   163 			if([name isEqualToString:@"chat"])
   164 			{
   165 				[mySN release];
   166 				mySN = [[attributes objectForKey:@"account"] retain];
   167 				
   168 				[service release];
   169 				service = [[attributes objectForKey:@"service"] retain];
   170 				
   171 				[myDisplayName release];
   172 				myDisplayName = nil;
   173 				
   174 				for (AIAccount *account in adium.accountController.accounts) {
   175 					if ([[account.UID compactedString] isEqualToString:[mySN compactedString]] &&
   176 						[account.service.serviceID isEqualToString:service]) {
   177 						myDisplayName = [account.displayName retain];
   178 						break;
   179 					}
   180 				}
   181 
   182 				state = XML_STATE_CHAT;
   183 			}
   184 			break;
   185 		case XML_STATE_CHAT:
   186 			if([name isEqualToString:@"message"])
   187 			{
   188 				[sender release];
   189 				[senderAlias release];
   190 				[date release];
   191 				
   192 				NSString *dateStr = [attributes objectForKey:@"time"];
   193 				if(dateStr != nil)
   194 					date = [[NSCalendarDate calendarDateWithString:dateStr] retain];
   195 				else
   196 					date = nil;
   197 				sender = [[attributes objectForKey:@"sender"] retain];
   198 				senderAlias = [[attributes objectForKey:@"alias"] retain];
   199 				autoResponse = [[attributes objectForKey:@"auto"] isEqualToString:@"true"];
   200 
   201 				//Mark the location of the message...  We can copy it directly.  Anyone know why it is off by 1?
   202 				messageStart = CFXMLParserGetLocation(parser) - 1;
   203 				
   204 				state = XML_STATE_MESSAGE;
   205 			}
   206 			else if([name isEqualToString:@"event"])
   207 			{
   208 				//Mark the location of the message...  We can copy it directly.  Anyone know why it is off by 1?
   209 				messageStart = CFXMLParserGetLocation(parser) - 1;
   210 
   211 				state = XML_STATE_EVENT_MESSAGE;
   212 			}
   213 			else if([name isEqualToString:@"status"])
   214 			{
   215 				[status release];
   216 				[date release];
   217 				
   218 				NSString *dateStr = [attributes objectForKey:@"time"];
   219 				if(dateStr != nil)
   220 					date = [[NSCalendarDate calendarDateWithString:dateStr] retain];
   221 				else
   222 					date = nil;
   223 				
   224 				status = [[attributes objectForKey:@"type"] retain];
   225 
   226 				//Mark the location of the message...  We can copy it directly.  Anyone know why it is off by 1?
   227 				messageStart = CFXMLParserGetLocation(parser) - 1;
   228 
   229 				state = XML_STATE_STATUS_MESSAGE;
   230 			}
   231 			break;
   232 		case XML_STATE_MESSAGE:
   233 		case XML_STATE_EVENT_MESSAGE:
   234 		case XML_STATE_STATUS_MESSAGE:
   235 			break;
   236 	}
   237 }
   238 
   239 - (void)endedElement:(NSString *)name empty:(BOOL)empty
   240 {
   241 	switch(state)
   242 	{
   243 		case XML_STATE_EVENT_MESSAGE:
   244 			state = XML_STATE_CHAT;
   245 			break;
   246 
   247 		case XML_STATE_MESSAGE:
   248 			if([name isEqualToString:@"message"])
   249 			{
   250 				CFIndex end = CFXMLParserGetLocation(parser);
   251 				NSString *message = nil;
   252 				if (!empty) {
   253 					/* Need to unescape & now so that we'll do link detection properly when decoding the HTML. See #6850.
   254 					 * We'll let HTML decoding handle the other entities.
   255 					 *
   256 					 * 11 = 10 for </message> and 1 for the index being off
   257 					 */
   258 					NSMutableString *mutableMessage = [[inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 11)] mutableCopy];
   259 					[mutableMessage replaceOccurrencesOfString:@"&amp;"
   260 													withString:@"&"
   261 													   options:NSLiteralSearch
   262 														 range:NSMakeRange(0, [mutableMessage length])];
   263 					// Escape anchor tags
   264 					[mutableMessage replaceOccurrencesOfString:@"#"
   265 													withString:@"&#x23;"
   266 													   options:NSLiteralSearch
   267 														 range:NSMakeRange(0, [mutableMessage length])];
   268 					message = [mutableMessage autorelease];
   269 				}
   270 				NSString *shownSender = (senderAlias ? senderAlias : sender);
   271 				NSString *cssClass;
   272 				NSString *displayName = nil, *longDisplayName = nil;
   273 				
   274 				if ([mySN isEqualToString:sender]) {
   275 					//Find an account if one exists, and use its name
   276 					displayName = (myDisplayName ? myDisplayName : sender);
   277 					cssClass = @"send";
   278 				} else {
   279 					AIListObject *listObject = [adium.contactController existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:service UID:sender]];
   280 
   281 					cssClass = @"receive";
   282 					displayName = listObject.displayName;
   283 					longDisplayName = [listObject longDisplayName];
   284 				}
   285 
   286 				if (displayName && ![displayName isEqualToString:sender]) {
   287 					switch (nameFormat) {
   288 						case AIDefaultName:
   289 							shownSender = (longDisplayName ? longDisplayName : displayName);
   290 							break;
   291 
   292 						case AIDisplayName:
   293 							shownSender = displayName;
   294 							break;
   295 
   296 						case AIDisplayName_ScreenName:
   297 							shownSender = [NSString stringWithFormat:@"%@ (%@)",displayName,sender];
   298 							break;
   299 
   300 						case AIScreenName_DisplayName:
   301 							shownSender = [NSString stringWithFormat:@"%@ (%@)",sender,displayName];
   302 							break;
   303 
   304 						case AIScreenName:
   305 							shownSender = sender;
   306 							break;	
   307 					}
   308 				}
   309 				
   310 				NSString *timestampStr = [dateFormatter stringFromDate:date];
   311 				
   312 				BOOL sentMessage = [mySN isEqualToString:sender];
   313 				[output appendAttributedString:[htmlDecoder decodeHTML:[NSString stringWithFormat:
   314 										 @"<div class=\"%@\">%@<span class=\"sender\">%@%@:</span></div> ",
   315 										 (sentMessage ? @"send" : @"receive"),
   316 										 (showTimestamps ? [NSString stringWithFormat:@"<span class=\"timestamp\">%@</span> ", timestampStr] : @""),
   317 										 shownSender, (autoResponse ? AILocalizedString(@" (Autoreply)", nil) : @"")]]];
   318 				
   319 				NSAttributedString *attributedMessage = [htmlDecoder decodeHTML:message];
   320 				if (showEmoticons) {
   321 					attributedMessage = [adium.contentController filterAttributedString:attributedMessage
   322 																		  usingFilterType:AIFilterMessageDisplay
   323 																				direction:(sentMessage ? AIFilterOutgoing : AIFilterIncoming)
   324 																				  context:nil];				
   325 				}
   326 				[output appendAttributedString:attributedMessage];
   327 				[output appendAttributedString:newlineAttributedString];
   328 
   329 				state = XML_STATE_CHAT;
   330 			}
   331 			break;
   332 		case XML_STATE_STATUS_MESSAGE:
   333 			if([name isEqualToString:@"status"])
   334 			{
   335 				CFIndex end = CFXMLParserGetLocation(parser);
   336 				NSString *message = nil;
   337 				if(!empty)
   338 					message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 10)];  // 9 for </status> and 1 for the index being off
   339 								
   340 				NSString *displayMessage = nil;
   341 				//Note: I am diverging from what the AILoggerPlugin logs in this case.  It can't handle every case we can have here
   342 				if([message length])
   343 				{
   344 					if([statusLookup objectForKey:status])
   345 						displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@: %@", nil), [statusLookup objectForKey:status], message];
   346 					else
   347 						displayMessage = [NSString stringWithFormat:AILocalizedString(@"%@", nil), message];
   348 				}
   349 				else if([status length] && [statusLookup objectForKey:status])
   350 					displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@", nil), [statusLookup objectForKey:status]];
   351 
   352 				if([displayMessage length])
   353 					[output appendAttributedString:[htmlDecoder decodeHTML:[NSString stringWithFormat:@"<div class=\"status\">%@ (%@)</div>\n",
   354 																			displayMessage,
   355 																			[dateFormatter stringFromDate:date]]]];
   356 					state = XML_STATE_CHAT;
   357 			}			
   358 		case XML_STATE_CHAT:
   359 			if([name isEqualToString:@"chat"])
   360 				state = XML_STATE_NONE;
   361 			break;
   362 		case XML_STATE_NONE:
   363 			break;
   364 	}
   365 }
   366 
   367 typedef struct{
   368 	NSString	*name;
   369 	BOOL		empty;
   370 } element;
   371 
   372 void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context)
   373 {
   374 	element *ret = NULL;
   375 	
   376     // Use the dataTypeID to determine what to print.
   377     switch (CFXMLNodeGetTypeCode(node)) {
   378         case kCFXMLNodeTypeDocument:
   379             break;
   380         case kCFXMLNodeTypeElement:
   381 		{
   382 			NSString *name = [NSString stringWithString:(NSString *)CFXMLNodeGetString(node)];
   383 			const CFXMLElementInfo *info = CFXMLNodeGetInfoPtr(node);
   384 			[(AIXMLChatlogConverter *)context startedElement:name info:info];
   385 			ret = (element *)malloc(sizeof(element));
   386 			ret->name = [name retain];
   387 			ret->empty = info->isEmpty;
   388 			break;
   389 		}
   390         case kCFXMLNodeTypeProcessingInstruction:
   391         case kCFXMLNodeTypeComment:
   392         case kCFXMLNodeTypeText:
   393         case kCFXMLNodeTypeCDATASection:
   394         case kCFXMLNodeTypeEntityReference:
   395         case kCFXMLNodeTypeDocumentType:
   396         case kCFXMLNodeTypeWhitespace:
   397         default:
   398 			break;
   399 	}
   400 	
   401     // Return the data string for use by the addChild and 
   402     // endStructure callbacks.
   403     return (void *) ret;
   404 }
   405 
   406 void addChild(CFXMLParserRef parser, void *parent, void *child, void *context)
   407 {
   408 }
   409 
   410 void endStructure(CFXMLParserRef parser, void *xmlType, void *context)
   411 {
   412 	NSString *name = nil;
   413 	BOOL empty = NO;
   414 	if(xmlType != NULL)
   415 	{
   416 		name = [NSString stringWithString:((element *)xmlType)->name];
   417 		empty = ((element *)xmlType)->empty;
   418 	}
   419 	[(AIXMLChatlogConverter *)context endedElement:name empty:empty];
   420 	if(xmlType != NULL)
   421 	{
   422 		[((element *)xmlType)->name release];
   423 		free(xmlType);
   424 	}
   425 }
   426 
   427 @end