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.
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
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.
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.
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.
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>
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"
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);
38 @implementation AIXMLChatlogConverter
40 + (NSAttributedString *)readFile:(NSString *)filePath withOptions:(NSDictionary *)options
42 AIXMLChatlogConverter *converter = [[AIXMLChatlogConverter alloc] init];
43 NSAttributedString *ret = [[converter readFile:filePath withOptions:options] retain];
45 return [ret autorelease];
50 if ((self = [super init])) {
52 state = XML_STATE_NONE;
54 inputFileString = nil;
62 dateFormatter = [[NSDateFormatter localizedDateFormatterShowingSeconds:YES showingAMorPM:YES] retain];
64 newlineAttributedString = [[NSAttributedString alloc] initWithString:@"\n" attributes:nil];
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",
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];
90 nameFormat = AIDefaultName;
99 [dateFormatter release];
100 [newlineAttributedString release];
101 [inputFileString release];
102 [eventTranslate release];
104 [senderAlias release];
106 [myDisplayName release];
111 [statusLookup release];
112 [htmlDecoder release];
116 - (NSAttributedString *)readFile:(NSString *)filePath withOptions:(NSDictionary *)options
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];
123 htmlDecoder = [[AIHTMLDecoder alloc] init];
124 [htmlDecoder setBaseURL:[filePath stringByDeletingLastPathComponent]];
126 showTimestamps = [[options objectForKey:@"showTimestamps"] boolValue];
127 showEmoticons = [[options objectForKey:@"showEmoticons"] boolValue];
129 CFXMLParserCallBacks callbacks = {
137 CFXMLParserContext context = {
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);
157 - (void)startedElement:(NSString *)name info:(const CFXMLElementInfo *)info
159 NSDictionary *attributes = (NSDictionary *)info->attributes;
163 if([name isEqualToString:@"chat"])
166 mySN = [[attributes objectForKey:@"account"] retain];
169 service = [[attributes objectForKey:@"service"] retain];
171 [myDisplayName release];
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];
182 state = XML_STATE_CHAT;
186 if([name isEqualToString:@"message"])
189 [senderAlias release];
192 NSString *dateStr = [attributes objectForKey:@"time"];
194 date = [[NSCalendarDate calendarDateWithString:dateStr] retain];
197 sender = [[attributes objectForKey:@"sender"] retain];
198 senderAlias = [[attributes objectForKey:@"alias"] retain];
199 autoResponse = [[attributes objectForKey:@"auto"] isEqualToString:@"true"];
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;
204 state = XML_STATE_MESSAGE;
206 else if([name isEqualToString:@"event"])
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;
211 state = XML_STATE_EVENT_MESSAGE;
213 else if([name isEqualToString:@"status"])
218 NSString *dateStr = [attributes objectForKey:@"time"];
220 date = [[NSCalendarDate calendarDateWithString:dateStr] retain];
224 status = [[attributes objectForKey:@"type"] retain];
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;
229 state = XML_STATE_STATUS_MESSAGE;
232 case XML_STATE_MESSAGE:
233 case XML_STATE_EVENT_MESSAGE:
234 case XML_STATE_STATUS_MESSAGE:
239 - (void)endedElement:(NSString *)name empty:(BOOL)empty
243 case XML_STATE_EVENT_MESSAGE:
244 state = XML_STATE_CHAT;
247 case XML_STATE_MESSAGE:
248 if([name isEqualToString:@"message"])
250 CFIndex end = CFXMLParserGetLocation(parser);
251 NSString *message = nil;
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.
256 * 11 = 10 for </message> and 1 for the index being off
258 NSMutableString *mutableMessage = [[inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 11)] mutableCopy];
259 [mutableMessage replaceOccurrencesOfString:@"&"
261 options:NSLiteralSearch
262 range:NSMakeRange(0, [mutableMessage length])];
263 // Escape anchor tags
264 [mutableMessage replaceOccurrencesOfString:@"#"
266 options:NSLiteralSearch
267 range:NSMakeRange(0, [mutableMessage length])];
268 message = [mutableMessage autorelease];
270 NSString *shownSender = (senderAlias ? senderAlias : sender);
272 NSString *displayName = nil, *longDisplayName = nil;
274 if ([mySN isEqualToString:sender]) {
275 //Find an account if one exists, and use its name
276 displayName = (myDisplayName ? myDisplayName : sender);
279 AIListObject *listObject = [adium.contactController existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:service UID:sender]];
281 cssClass = @"receive";
282 displayName = listObject.displayName;
283 longDisplayName = [listObject longDisplayName];
286 if (displayName && ![displayName isEqualToString:sender]) {
287 switch (nameFormat) {
289 shownSender = (longDisplayName ? longDisplayName : displayName);
293 shownSender = displayName;
296 case AIDisplayName_ScreenName:
297 shownSender = [NSString stringWithFormat:@"%@ (%@)",displayName,sender];
300 case AIScreenName_DisplayName:
301 shownSender = [NSString stringWithFormat:@"%@ (%@)",sender,displayName];
305 shownSender = sender;
310 NSString *timestampStr = [dateFormatter stringFromDate:date];
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) : @"")]]];
319 NSAttributedString *attributedMessage = [htmlDecoder decodeHTML:message];
321 attributedMessage = [adium.contentController filterAttributedString:attributedMessage
322 usingFilterType:AIFilterMessageDisplay
323 direction:(sentMessage ? AIFilterOutgoing : AIFilterIncoming)
326 [output appendAttributedString:attributedMessage];
327 [output appendAttributedString:newlineAttributedString];
329 state = XML_STATE_CHAT;
332 case XML_STATE_STATUS_MESSAGE:
333 if([name isEqualToString:@"status"])
335 CFIndex end = CFXMLParserGetLocation(parser);
336 NSString *message = nil;
338 message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 10)]; // 9 for </status> and 1 for the index being off
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
344 if([statusLookup objectForKey:status])
345 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@: %@", nil), [statusLookup objectForKey:status], message];
347 displayMessage = [NSString stringWithFormat:AILocalizedString(@"%@", nil), message];
349 else if([status length] && [statusLookup objectForKey:status])
350 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@", nil), [statusLookup objectForKey:status]];
352 if([displayMessage length])
353 [output appendAttributedString:[htmlDecoder decodeHTML:[NSString stringWithFormat:@"<div class=\"status\">%@ (%@)</div>\n",
355 [dateFormatter stringFromDate:date]]]];
356 state = XML_STATE_CHAT;
359 if([name isEqualToString:@"chat"])
360 state = XML_STATE_NONE;
372 void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context)
376 // Use the dataTypeID to determine what to print.
377 switch (CFXMLNodeGetTypeCode(node)) {
378 case kCFXMLNodeTypeDocument:
380 case kCFXMLNodeTypeElement:
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;
390 case kCFXMLNodeTypeProcessingInstruction:
391 case kCFXMLNodeTypeComment:
392 case kCFXMLNodeTypeText:
393 case kCFXMLNodeTypeCDATASection:
394 case kCFXMLNodeTypeEntityReference:
395 case kCFXMLNodeTypeDocumentType:
396 case kCFXMLNodeTypeWhitespace:
401 // Return the data string for use by the addChild and
402 // endStructure callbacks.
406 void addChild(CFXMLParserRef parser, void *parent, void *child, void *context)
410 void endStructure(CFXMLParserRef parser, void *xmlType, void *context)
412 NSString *name = nil;
416 name = [NSString stringWithString:((element *)xmlType)->name];
417 empty = ((element *)xmlType)->empty;
419 [(AIXMLChatlogConverter *)context endedElement:name empty:empty];
422 [((element *)xmlType)->name release];