At some point in time, the need to replace & with & to allow linking to work died off. Do not try to mess with the URL at all. Fixes #13382.
Because & was being replaced with &, any following # were considered part of a &#…; sequence, which was causing some serious misparsing. By removing both the &->& and #->#, we kill two birds with one stone.
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 // 11 = 10 for </message> and 1 for the index being off
254 message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 11)];
256 NSString *shownSender = (senderAlias ? senderAlias : sender);
258 NSString *displayName = nil, *longDisplayName = nil;
260 if ([mySN isEqualToString:sender]) {
261 //Find an account if one exists, and use its name
262 displayName = (myDisplayName ? myDisplayName : sender);
265 AIListObject *listObject = [adium.contactController existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:service UID:sender]];
267 cssClass = @"receive";
268 displayName = listObject.displayName;
269 longDisplayName = [listObject longDisplayName];
272 if (displayName && ![displayName isEqualToString:sender]) {
273 switch (nameFormat) {
275 shownSender = (longDisplayName ? longDisplayName : displayName);
279 shownSender = displayName;
282 case AIDisplayName_ScreenName:
283 shownSender = [NSString stringWithFormat:@"%@ (%@)",displayName,sender];
286 case AIScreenName_DisplayName:
287 shownSender = [NSString stringWithFormat:@"%@ (%@)",sender,displayName];
291 shownSender = sender;
296 NSString *timestampStr = [dateFormatter stringFromDate:date];
298 BOOL sentMessage = [mySN isEqualToString:sender];
299 [output appendAttributedString:[htmlDecoder decodeHTML:[NSString stringWithFormat:
300 @"<div class=\"%@\">%@<span class=\"sender\">%@%@:</span></div> ",
301 (sentMessage ? @"send" : @"receive"),
302 (showTimestamps ? [NSString stringWithFormat:@"<span class=\"timestamp\">%@</span> ", timestampStr] : @""),
303 shownSender, (autoResponse ? AILocalizedString(@" (Autoreply)", nil) : @"")]]];
305 NSAttributedString *attributedMessage = [htmlDecoder decodeHTML:message];
307 attributedMessage = [adium.contentController filterAttributedString:attributedMessage
308 usingFilterType:AIFilterMessageDisplay
309 direction:(sentMessage ? AIFilterOutgoing : AIFilterIncoming)
312 [output appendAttributedString:attributedMessage];
313 [output appendAttributedString:newlineAttributedString];
315 state = XML_STATE_CHAT;
318 case XML_STATE_STATUS_MESSAGE:
319 if([name isEqualToString:@"status"])
321 CFIndex end = CFXMLParserGetLocation(parser);
322 NSString *message = nil;
324 message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 10)]; // 9 for </status> and 1 for the index being off
326 NSString *displayMessage = nil;
327 //Note: I am diverging from what the AILoggerPlugin logs in this case. It can't handle every case we can have here
330 if([statusLookup objectForKey:status])
331 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@: %@", nil), [statusLookup objectForKey:status], message];
333 displayMessage = [NSString stringWithFormat:AILocalizedString(@"%@", nil), message];
335 else if([status length] && [statusLookup objectForKey:status])
336 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@", nil), [statusLookup objectForKey:status]];
338 if([displayMessage length])
339 [output appendAttributedString:[htmlDecoder decodeHTML:[NSString stringWithFormat:@"<div class=\"status\">%@ (%@)</div>\n",
341 [dateFormatter stringFromDate:date]]]];
342 state = XML_STATE_CHAT;
345 if([name isEqualToString:@"chat"])
346 state = XML_STATE_NONE;
358 void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context)
362 // Use the dataTypeID to determine what to print.
363 switch (CFXMLNodeGetTypeCode(node)) {
364 case kCFXMLNodeTypeDocument:
366 case kCFXMLNodeTypeElement:
368 NSString *name = [NSString stringWithString:(NSString *)CFXMLNodeGetString(node)];
369 const CFXMLElementInfo *info = CFXMLNodeGetInfoPtr(node);
370 [(AIXMLChatlogConverter *)context startedElement:name info:info];
371 ret = (element *)malloc(sizeof(element));
372 ret->name = [name retain];
373 ret->empty = info->isEmpty;
376 case kCFXMLNodeTypeProcessingInstruction:
377 case kCFXMLNodeTypeComment:
378 case kCFXMLNodeTypeText:
379 case kCFXMLNodeTypeCDATASection:
380 case kCFXMLNodeTypeEntityReference:
381 case kCFXMLNodeTypeDocumentType:
382 case kCFXMLNodeTypeWhitespace:
387 // Return the data string for use by the addChild and
388 // endStructure callbacks.
392 void addChild(CFXMLParserRef parser, void *parent, void *child, void *context)
396 void endStructure(CFXMLParserRef parser, void *xmlType, void *context)
398 NSString *name = nil;
402 name = [NSString stringWithString:((element *)xmlType)->name];
403 empty = ((element *)xmlType)->empty;
405 [(AIXMLChatlogConverter *)context endedElement:name empty:empty];
408 [((element *)xmlType)->name release];