Prevent sending 0-byte files. Fixes #8711.
[adiumx.git] / Source / GBChatlogHTMLConverter.m
blob813240908da8a74f01be9aa7045fa29f3f3ca98a
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  */
17 #import "GBChatlogHTMLConverter.h"
18 #import "AIStandardListWindowController.h"
19 #import <Adium/AIListContact.h>
20 #import <Adium/AIAccountControllerProtocol.h>
21 #import <Adium/AIContactControllerProtocol.h>
22 #import <Adium/AIPreferenceControllerProtocol.h>
23 #import <Adium/AIStatusControllerProtocol.h>
24 #import <AIUtilities/NSCalendarDate+ISO8601Parsing.h>
25 #import <AIUtilities/AIDateFormatterAdditions.h>
26 #import <AIUtilities/AIStringAdditions.h>
29 #define PREF_GROUP_WEBKIT_MESSAGE_DISPLAY               @"WebKit Message Display"
30 #define KEY_WEBKIT_USE_NAME_FORMAT                              @"Use Custom Name Format"
31 #define KEY_WEBKIT_NAME_FORMAT                                  @"Name Format"
33 static void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context);
34 static void addChild(CFXMLParserRef parser, void *parent, void *child, void *context);
35 static void endStructure(CFXMLParserRef parser, void *xmlType, void *context);
37 @implementation GBChatlogHTMLConverter
39 + (NSString *)readFile:(NSString *)filePath
41         GBChatlogHTMLConverter *converter = [[GBChatlogHTMLConverter alloc] init];
42         NSString *ret = [[converter readFile:filePath] retain];
43         [converter release];
44         return [ret autorelease];
47 - (id)init
49         self = [super init];
50         if(self == nil)
51                 return nil;
52         
53         state = XML_STATE_NONE;
54         
55         inputFileString = nil;
56         sender = nil;
57         mySN = nil;
58         myDisplayName = nil;
59         date = nil;
60         parser = NULL;
61         status = nil;
62         
63         statusLookup = [[NSDictionary alloc] initWithObjectsAndKeys:
64                 AILocalizedString(@"Online", nil), @"online",
65                 AILocalizedString(@"Idle", nil), @"idle",
66                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_OFFLINE], @"offline",
67                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_AWAY], @"away",
68                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_AVAILABLE], @"available",
69                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_BUSY], @"busy",
70                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AT_HOME], @"notAtHome",
71                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_PHONE], @"onThePhone",
72                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_VACATION], @"onVacation",
73                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_DND], @"doNotDisturb",
74                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_EXTENDED_AWAY], @"extendedAway",
75                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_BRB], @"beRightBack",
76                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AVAILABLE], @"notAvailable",
77                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AT_DESK], @"notAtMyDesk",
78                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_IN_OFFICE], @"notInTheOffice",
79                 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_STEPPED_OUT], @"steppedOut",
80                 nil];
81                 
82         if ([[[adium preferenceController] preferenceForKey:KEY_WEBKIT_USE_NAME_FORMAT
83                                                                                                   group:PREF_GROUP_WEBKIT_MESSAGE_DISPLAY] boolValue]) {
84                 nameFormat = [[[adium preferenceController] preferenceForKey:KEY_WEBKIT_NAME_FORMAT
85                                                                                                                            group:PREF_GROUP_WEBKIT_MESSAGE_DISPLAY] intValue];
86         } else {
87                 nameFormat = AIDefaultName;
88         }
90         return self;
93 - (void)dealloc
95         [inputFileString release];
96         [eventTranslate release];
97         [sender release];
98         [mySN release];
99         [myDisplayName release];
100         [service release];
101         [date release];
102         [status release];
103         [output release];
104         [statusLookup release];
105         [super dealloc];
108 - (NSString *)readFile:(NSString *)filePath
110         NSData *inputData = [NSData dataWithContentsOfFile:filePath];
111         inputFileString = [[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding];
112         NSURL *url = [[NSURL alloc] initFileURLWithPath:filePath];
113         output = [[NSMutableString alloc] init];
114         
115         CFXMLParserCallBacks callbacks = {
116                 0,
117                 createStructure,
118                 addChild,
119                 endStructure,
120                 NULL,
121                 NULL
122         };
123         CFXMLParserContext context = {
124                 0,
125                 self,
126                 CFRetain,
127                 CFRelease,
128                 NULL
129         };
130         parser = CFXMLParserCreateWithDataFromURL(NULL, (CFURLRef)url, kCFXMLParserSkipMetaData | kCFXMLParserSkipWhitespace, kCFXMLNodeCurrentVersion, &callbacks, &context);
131         if (!CFXMLParserParse(parser)) {
132                 NSLog(@"%@: Parser %@ for inputFileString %@ returned false.",
133                           [self class], parser, inputFileString);
134                 [output release];
135                 output = nil;
136         }
137         CFRelease(parser);
138         parser = nil;
139         [url release];
140         return output;
143 - (void)startedElement:(NSString *)name info:(const CFXMLElementInfo *)info
145         NSDictionary *attributes = (NSDictionary *)info->attributes;
146         
147         switch(state){
148                 case XML_STATE_NONE:
149                         if([name isEqualToString:@"chat"])
150                         {
151                                 [mySN release];
152                                 mySN = [[attributes objectForKey:@"account"] retain];
153                                 
154                                 [service release];
155                                 service = [[attributes objectForKey:@"service"] retain];
156                                 
157                                 [myDisplayName release];
158                                 myDisplayName = nil;
159                                 
160                                 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
161                                 AIAccount        *account;
162                                 
163                                 while ((account = [enumerator nextObject])) {
164                                         if ([[[account UID] compactedString] isEqualToString:[mySN compactedString]] &&
165                                                 [[account serviceID] isEqualToString:service]) {
166                                                 myDisplayName = [[account displayName] retain];
167                                         }
168                                 }
170                                 state = XML_STATE_CHAT;
171                         }
172                         break;
173                 case XML_STATE_CHAT:
174                         if([name isEqualToString:@"message"])
175                         {
176                                 [sender release];
177                                 [date release];
178                                 
179                                 NSString *dateStr = [attributes objectForKey:@"time"];
180                                 if(dateStr != nil)
181                                         date = [[NSCalendarDate calendarDateWithString:dateStr] retain];
182                                 else
183                                         date = nil;
184                                 sender = [[attributes objectForKey:@"sender"] retain];
185                                 autoResponse = [[attributes objectForKey:@"auto"] isEqualToString:@"true"];
187                                 //Mark the location of the message...  We can copy it directly.  Anyone know why it is off by 1?
188                                 messageStart = CFXMLParserGetLocation(parser) - 1;
189                                 
190                                 state = XML_STATE_MESSAGE;
191                         }
192                         else if([name isEqualToString:@"event"])
193                         {
194                                 //Mark the location of the message...  We can copy it directly.  Anyone know why it is off by 1?
195                                 messageStart = CFXMLParserGetLocation(parser) - 1;
197                                 state = XML_STATE_EVENT_MESSAGE;
198                         }
199                         else if([name isEqualToString:@"status"])
200                         {
201                                 [status release];
202                                 [date release];
203                                 
204                                 NSString *dateStr = [attributes objectForKey:@"time"];
205                                 if(dateStr != nil)
206                                         date = [[NSCalendarDate calendarDateWithString:dateStr] retain];
207                                 else
208                                         date = nil;
209                                 
210                                 status = [[attributes objectForKey:@"type"] retain];
212                                 //Mark the location of the message...  We can copy it directly.  Anyone know why it is off by 1?
213                                 messageStart = CFXMLParserGetLocation(parser) - 1;
215                                 state = XML_STATE_STATUS_MESSAGE;
216                         }
217                         break;
218                 case XML_STATE_MESSAGE:
219                 case XML_STATE_EVENT_MESSAGE:
220                 case XML_STATE_STATUS_MESSAGE:
221                         break;
222         }
225 - (void)endedElement:(NSString *)name empty:(BOOL)empty
227         switch(state)
228         {
229                 case XML_STATE_EVENT_MESSAGE:
230                         state = XML_STATE_CHAT;
231                         break;
233                 case XML_STATE_MESSAGE:
234                         if([name isEqualToString:@"message"])
235                         {
236                                 CFIndex end = CFXMLParserGetLocation(parser);
237                                 NSString *message = nil;
238                                 if (!empty) {
239                                         // Need to unescape entities ourself. & wasn't getting unescaped for some reason. See #6850.
240                                         // 10 for </message> and 1 for the index being off
241                                         message = [[inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 11)] stringByUnescapingFromXMLWithEntities:nil];
242                                 }
243                                 NSString *shownSender = sender;
244                                 NSString *cssClass;
245                                 NSString *displayName = nil, *longDisplayName = nil;
246                                 
247                                 if ([mySN isEqualToString:sender]) {
248                                         //Find an account if one exists, and use its name
249                                         displayName = (myDisplayName ? myDisplayName : sender);
250                                         cssClass = @"send";
251                                 } else {
252                                         AIListObject *listObject = [[adium contactController] existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:service UID:sender]];
254                                         cssClass = @"receive";
255                                         displayName = [listObject displayName];
256                                         longDisplayName = [listObject longDisplayName];
257                                 }
259                                 if (displayName && ![displayName isEqualToString:sender]) {
260                                         switch (nameFormat) {
261                                                 case AIDefaultName:
262                                                         shownSender = (longDisplayName ? longDisplayName : displayName);
263                                                         break;
265                                                 case AIDisplayName:
266                                                         shownSender = displayName;
267                                                         break;
269                                                 case AIDisplayName_ScreenName:
270                                                         shownSender = [NSString stringWithFormat:@"%@ (%@)",displayName,sender];
271                                                         break;
273                                                 case AIScreenName_DisplayName:
274                                                         shownSender = [NSString stringWithFormat:@"%@ (%@)",sender,displayName];
275                                                         break;
277                                                 case AIScreenName:
278                                                         shownSender = sender;
279                                                         break;  
280                                         }
281                                 }
282                                 
283                                 [output appendFormat:@"<div class=\"%@\"><span class=\"timestamp\">%@</span> <span class=\"sender\">%@%@: </span><pre class=\"message\">%@</pre></div>\n",
284                                         ([mySN isEqualToString:sender] ? @"send" : @"receive"), 
285                                         [date descriptionWithCalendarFormat:[NSDateFormatter localizedDateFormatStringShowingSeconds:YES
286                                                                                                                                                                                                    showingAMorPM:YES]
287                                                                                            timeZone:nil
288                                                                                                  locale:nil],
289                                         shownSender, 
290                                         (autoResponse ? AILocalizedString(@" (Autoreply)",nil) : @""),
291                                         message];
292                                 state = XML_STATE_CHAT;
293                         }
294                         break;
295                 case XML_STATE_STATUS_MESSAGE:
296                         if([name isEqualToString:@"status"])
297                         {
298                                 CFIndex end = CFXMLParserGetLocation(parser);
299                                 NSString *message = nil;
300                                 if(!empty)
301                                         message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 10)];  // 9 for </status> and 1 for the index being off
302                                                                 
303                                 NSString *displayMessage = nil;
304                                 //Note: I am diverging from what the AILoggerPlugin logs in this case.  It can't handle every case we can have here
305                                 if([message length])
306                                 {
307                                         if([status length])
308                                                 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@: %@", nil), [statusLookup objectForKey:status], message];
309                                         else
310                                                 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@", nil), message];
311                                 }
312                                 else if([status length])
313                                         displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@", nil), [statusLookup objectForKey:status]];
315                                 if([displayMessage length])
316                                         [output appendFormat:@"<div class=\"status\">%@ (%@)</div>\n",
317                                                 displayMessage,
318                                                 [date descriptionWithCalendarFormat:[NSDateFormatter localizedDateFormatStringShowingSeconds:YES
319                                                                                                                                                                                                            showingAMorPM:YES]
320                                                                                                    timeZone:nil
321                                                                                                          locale:nil]];
322                                 state = XML_STATE_CHAT;
323                         }                       
324                 case XML_STATE_CHAT:
325                         if([name isEqualToString:@"chat"])
326                                 state = XML_STATE_NONE;
327                         break;
328                 case XML_STATE_NONE:
329                         break;
330         }
333 typedef struct{
334         NSString        *name;
335         BOOL            empty;
336 } element;
338 void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context)
340         element *ret = NULL;
341         
342     // Use the dataTypeID to determine what to print.
343     switch (CFXMLNodeGetTypeCode(node)) {
344         case kCFXMLNodeTypeDocument:
345             break;
346         case kCFXMLNodeTypeElement:
347                 {
348                         NSString *name = [NSString stringWithString:(NSString *)CFXMLNodeGetString(node)];
349                         const CFXMLElementInfo *info = CFXMLNodeGetInfoPtr(node);
350                         [(GBChatlogHTMLConverter *)context startedElement:name info:info];
351                         ret = (element *)malloc(sizeof(element));
352                         ret->name = [name retain];
353                         ret->empty = info->isEmpty;
354                         break;
355                 }
356         case kCFXMLNodeTypeProcessingInstruction:
357         case kCFXMLNodeTypeComment:
358         case kCFXMLNodeTypeText:
359         case kCFXMLNodeTypeCDATASection:
360         case kCFXMLNodeTypeEntityReference:
361         case kCFXMLNodeTypeDocumentType:
362         case kCFXMLNodeTypeWhitespace:
363         default:
364                         break;
365         }
366         
367     // Return the data string for use by the addChild and 
368     // endStructure callbacks.
369     return (void *) ret;
372 void addChild(CFXMLParserRef parser, void *parent, void *child, void *context)
376 void endStructure(CFXMLParserRef parser, void *xmlType, void *context)
378         NSString *name = nil;
379         BOOL empty = NO;
380         if(xmlType != NULL)
381         {
382                 name = [NSString stringWithString:((element *)xmlType)->name];
383                 empty = ((element *)xmlType)->empty;
384         }
385         [(GBChatlogHTMLConverter *)context endedElement:name empty:empty];
386         if(xmlType != NULL)
387         {
388                 [((element *)xmlType)->name release];
389                 free(xmlType);
390         }
393 @end