Source/AIXMLChatlogConverter.m
author Zachary West <zacw@adium.im>
Mon Nov 23 00:46:43 2009 -0500 (2009-11-23)
changeset 2799 ba94093fbed8
parent 2597 07e07f9788a6
child 3249 135c4d1bee34
permissions -rw-r--r--
At some point in time, the need to replace &amp; with & to allow linking to work died off. Do not try to mess with the URL at all. Fixes #13382.

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