Added `-[NSArray validateAsPropertyList]` and `-[NSDictionary validateAsPropertyList...
[adiumx.git] / Source / ESFileTransferController.m
blob6e3eaa48fcf18b8d6ce2b89b1f66704e65074374
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 "ESFileTransferController.h"
19 #import <Adium/AIAccountControllerProtocol.h>
20 #import <Adium/AIContactControllerProtocol.h>
21 #import <Adium/AIInterfaceControllerProtocol.h>
22 #import <Adium/AIMenuControllerProtocol.h>
23 #import <Adium/AIToolbarControllerProtocol.h>
24 #import <Adium/AIContactAlertsControllerProtocol.h>
25 #import "ESFileTransferPreferences.h"
26 #import "ESFileTransferProgressWindowController.h"
27 #import "ESFileTransferRequestPromptController.h"
28 #import <AIUtilities/AIDictionaryAdditions.h>
29 #import <AIUtilities/AIFileManagerAdditions.h>
30 #import <AIUtilities/AIMenuAdditions.h>
31 #import <AIUtilities/AIStringAdditions.h>
32 #import <AIUtilities/AIToolbarUtilities.h>
33 #import <AIUtilities/AIObjectAdditions.h>
34 #import <AIUtilities/AIImageAdditions.h>
35 #import <Adium/AIAccount.h>
36 #import <Adium/AIListContact.h>
37 #import <Adium/AIListObject.h>
38 #import <Adium/AIListGroup.h>
39 #import "ESFileTransfer.h"
40 #import <Adium/AIWindowController.h>
42 #define SEND_FILE                                       AILocalizedString(@"Send File",nil)
43 #define SEND_FILE_WITH_ELLIPSIS         [SEND_FILE stringByAppendingEllipsis]
44 #define CONTACT                                         AILocalizedString(@"Contact",nil)
46 #define SEND_FILE_IDENTIFIER            @"SendFile"
48 #define FILE_TRANSFER_DEFAULT_PREFS     @"FileTransferPrefs"
50 #define SAFE_FILE_EXTENSIONS_SET        [NSSet setWithObjects:@"jpg",@"jpeg",@"gif",@"png",@"tif",@"tiff",@"psd",@"pdf",@"txt",@"rtf",@"html",@"htm",@"swf",@"mp3",@"wma",@"wmv",@"ogg",@"ogm",@"mov",@"mpg",@"mpeg",@"m1v",@"m2v",@"mp4",@"avi",@"vob",@"avi",@"asx",@"asf",@"pls",@"m3u",@"rmp",@"aif",@"aiff",@"aifc",@"wav",@"wave",@"m4a",@"m4p",@"m4b",@"dmg",@"udif",@"ndif",@"dart",@"sparseimage",@"cdr",@"dvdr",@"iso",@"img",@"toast",@"rar",@"sit",@"sitx",@"bin",@"hqx",@"zip",@"gz",@"tgz",@"tar",@"bz",@"bz2",@"tbz",@"z",@"taz",@"uu",@"uue",@"colloquytranscript",@"torrent",@"AdiumIcon",@"AdiumSoundset",@"AdiumEmoticon",@"AdiumMessageStyle",nil]
52 static ESFileTransferPreferences *preferences;
54 @interface ESFileTransferController (PRIVATE)
55 - (void)configureFileTransferProgressWindow;
56 - (void)showProgressWindow:(id)sender;
57 - (void)showProgressWindowIfNotOpen:(id)sender;
59 - (void)_finishReceiveRequestForFileTransfer:(ESFileTransfer *)fileTransfer localFilename:(NSString *)localFilename;
61 - (BOOL)shouldOpenCompleteFileTransfer:(ESFileTransfer *)fileTransfer;
62 @end
64 @implementation ESFileTransferController
66 //init
67 - (id)init
69         if ((self = [super init])) {
70                 fileTransferArray = [[NSMutableArray alloc] init];
71                 safeFileExtensions = nil;
72         }
73         
74         return self;
77 - (void)controllerDidLoad
79     //Add our get info contextual menu item
80     menuItem_sendFileContext = [[NSMenuItem alloc] initWithTitle:SEND_FILE_WITH_ELLIPSIS
81                                                                                                                   target:self action:@selector(contextualMenuSendFile:)
82                                                                                                    keyEquivalent:@""];
83         [[adium menuController] addContextualMenuItem:menuItem_sendFileContext toLocation:Context_Contact_Action];
84         
85         //Register the events we generate
86         NSObject <AIContactAlertsController> *contactAlertsController = [adium contactAlertsController];
87         [contactAlertsController registerEventID:FILE_TRANSFER_REQUEST withHandler:self inGroup:AIFileTransferEventHandlerGroup globalOnly:YES];
88         [contactAlertsController registerEventID:FILE_TRANSFER_WAITING_REMOTE withHandler:self inGroup:AIFileTransferEventHandlerGroup globalOnly:YES];
89         [contactAlertsController registerEventID:FILE_TRANSFER_BEGAN withHandler:self inGroup:AIFileTransferEventHandlerGroup globalOnly:YES];
90         [contactAlertsController registerEventID:FILE_TRANSFER_CANCELLED withHandler:self inGroup:AIFileTransferEventHandlerGroup globalOnly:YES];
91         [contactAlertsController registerEventID:FILE_TRANSFER_COMPLETE withHandler:self inGroup:AIFileTransferEventHandlerGroup globalOnly:YES];
92         [contactAlertsController registerEventID:FILE_TRANSFER_FAILED withHandler:self inGroup:AIFileTransferEventHandlerGroup globalOnly:NO];
94     //Install the Send File menu item
95         menuItem_sendFile = [[NSMenuItem alloc] initWithTitle:SEND_FILE_WITH_ELLIPSIS
96                                                                                                    target:self action:@selector(sendFileToSelectedContact:)
97                                                                                         keyEquivalent:@"F"];
98         [menuItem_sendFile setKeyEquivalentModifierMask:(NSCommandKeyMask | NSShiftKeyMask)];
99         [[adium menuController] addMenuItem:menuItem_sendFile toLocation:LOC_Contact_Action];
100         
101         //Add our "Send File" toolbar item
102         NSToolbarItem   *toolbarItem;
103     toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SEND_FILE_IDENTIFIER
104                                                                                                                   label:SEND_FILE
105                                                                                                    paletteLabel:SEND_FILE
106                                                                                                                 toolTip:AILocalizedString(@"Send a file","Tooltip for the Send File toolbar item")
107                                                                                                                  target:self
108                                                                                                 settingSelector:@selector(setImage:)
109                                                                                                         itemContent:[NSImage imageNamed:@"sendfile" forClass:[self class] loadLazily:YES]
110                                                                                                                  action:@selector(sendFileToSelectedContact:)
111                                                                                                                    menu:nil];
112     [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ListObject"];
113         
114     //Register our default preferences
115     [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:FILE_TRANSFER_DEFAULT_PREFS
116                                                                                                                                                 forClass:[self class]] 
117                                                                                   forGroup:PREF_GROUP_FILE_TRANSFER];
118     
119     //Observe pref changes
120         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_FILE_TRANSFER];
121         preferences = [[ESFileTransferPreferences preferencePane] retain];
122         
123         //Set up the file transfer progress window
124         [self configureFileTransferProgressWindow];
127 - (void)controllerWillClose
129     [[adium preferenceController] unregisterPreferenceObserver:self];
132 - (void)dealloc
134         [super dealloc];
135         
136         [safeFileExtensions release]; safeFileExtensions = nil;
137         [fileTransferArray release]; fileTransferArray = nil;
140 #pragma mark Access to file transfer objects
141 - (ESFileTransfer *)newFileTransferWithContact:(AIListContact *)inContact forAccount:(AIAccount *)inAccount type:(AIFileTransferType)t
143         ESFileTransfer *fileTransfer;
144         
145         fileTransfer = [ESFileTransfer fileTransferWithContact:inContact
146                                                                                                 forAccount:inAccount
147                                                                                                           type:t];
148         [fileTransferArray addObject:fileTransfer];
149         [fileTransfer setStatus:Not_Started_FileTransfer];
151         //Wait until the next run loop to inform observers of the new file transfer object;
152         //this way the code which requested a new ESFileTransfer has time to configure it before we
153         //dispaly information to the user
154         [[adium notificationCenter] performSelector:@selector(postNotificationName:object:)
155                                                                          withObject:FileTransfer_NewFileTransfer 
156                                                                          withObject:fileTransfer
157                                                                          afterDelay:0.0001];
159         return fileTransfer;
162 - (int)activeTransferCount
164         int count = 0;
165         ESFileTransfer *t;
166         NSEnumerator * fts = [fileTransferArray objectEnumerator];
168         while ((t = [fts nextObject])) {
169                 AIFileTransferStatus status = [t status];
171                 if ((status == Unknown_Status_FileTransfer) ||
172                         (status == Not_Started_FileTransfer) ||
173                         (status == Checksumming_Filetransfer) ||
174                         (status == Waiting_on_Remote_User_FileTransfer) ||
175                         (status == Connecting_FileTransfer) ||
176                         (status == Accepted_FileTransfer) ||
177                         (status == In_Progress_FileTransfer))
178                         count++;
179         }
180         return count;
184 - (NSArray *)fileTransferArray
186         return fileTransferArray;
189 //Remove a file transfer from our array.
190 - (void)_removeFileTransfer:(ESFileTransfer *)fileTransfer
192         [fileTransferArray removeObject:fileTransfer];
195 #pragma mark Sending and receiving
196 //Sent by an account when it gets a request for us to receive a file; prompt the user for a save location
197 - (void)receiveRequestForFileTransfer:(ESFileTransfer *)fileTransfer
199         AIListContact           *listContact = [fileTransfer contact];
200         NSString                        *localFilename = nil;
202         [fileTransfer setFileTransferType:Incoming_FileTransfer];
204         [[adium contactAlertsController] generateEvent:FILE_TRANSFER_REQUEST
205                                                                          forListObject:listContact
206                                                                                   userInfo:fileTransfer
207                                           previouslyPerformedActionIDs:nil];
209         if ((autoAcceptType == AutoAccept_All) ||
210            ((autoAcceptType == AutoAccept_FromContactList) && [listContact isIntentionallyNotAStranger])) {
211                 NSString        *preferredDownloadFolder = [[adium preferenceController] userPreferredDownloadFolder];
212                 NSString        *remoteFilename = [fileTransfer remoteFilename];
214                 //If the incoming file would become hidden, prefix it with an underscore so it is visible.
215                 if ([remoteFilename hasPrefix:@"."]) remoteFilename = [@"_" stringByAppendingString:remoteFilename ];
217                 //If we should autoaccept, determine the local filename and proceed to accept the request.
218                 localFilename = [preferredDownloadFolder stringByAppendingPathComponent:remoteFilename];
219                 
220                 [self _finishReceiveRequestForFileTransfer:fileTransfer
221                                                                          localFilename:[[NSFileManager defaultManager] uniquePathForPath:localFilename]];
222                         
223         } else {
224                 //Prompt to accept/deny
225                 [ESFileTransferRequestPromptController displayPromptForFileTransfer:fileTransfer
226                                                                                                                         notifyingTarget:self
227                                                                                                                                    selector:@selector(_finishReceiveRequestForFileTransfer:localFilename:)];
228         }
232  * @brief Finish the receive request process
234  * Called by either ESFileTransferRequestPromptController or self, this method is the last step in accepting or
235  * refusing a request to be sent a file.
237  * @param fileTransfer The file transfer in question
238  * @param localFilename Full path at which to save the file.  If anything exists at this path it will be overwritten without further confirmation.  Pass nil to deny the transfer.
239  */
240 - (void)_finishReceiveRequestForFileTransfer:(ESFileTransfer *)fileTransfer localFilename:(NSString *)localFilename
241 {       
242         if([fileTransfer isStopped]) //if it's been canceled while we were busy asking the user stuff, ignore it
243                 return;
244         if (localFilename) {
245                 [fileTransfer setLocalFilename:localFilename];
246                 [fileTransfer setStatus:Accepted_FileTransfer];
248                 [(AIAccount<AIAccount_Files> *)[fileTransfer account] acceptFileTransferRequest:fileTransfer];
249                 
250                 if (showProgressWindow) {
251                         [self showProgressWindowIfNotOpen:nil];
252                 }
253                 
254         } else {
255                 [(AIAccount<AIAccount_Files> *)[fileTransfer account] rejectFileReceiveRequest:fileTransfer];
256                 [fileTransfer setStatus:Cancelled_Local_FileTransfer];
257         }       
260 //Prompt the user for the file to send via an Open File dialogue
261 - (void)requestForSendingFileToListContact:(AIListContact *)listContact
263         NSOpenPanel *openPanel = [NSOpenPanel openPanel];
264         [openPanel setTitle:[NSString stringWithFormat:AILocalizedString(@"Send File to %@",nil),[listContact displayName]]];
265         [openPanel setCanChooseDirectories:YES];
266         [openPanel setResolvesAliases:YES];
267         [openPanel setAllowsMultipleSelection:YES];
268         [openPanel setPrompt:AILocalizedStringFromTable(@"Send", @"Buttons", nil)];
270         if ([openPanel runModalForDirectory:nil file:nil types:nil] == NSOKButton) {
271                 NSEnumerator *enumerator = [[openPanel filenames] objectEnumerator];
272                 NSString         *filePath;
274                 while ((filePath = [enumerator nextObject])) {
275                         [self sendFile:filePath toListContact:listContact];
276                 }
277         }
280 - (NSString *)pathToArchiveOfFolder:(NSString *)inPath
282         NSString                *pathToArchive = nil;
283         NSFileManager   *defaultManager = [NSFileManager defaultManager];
284         NSString                *tmpDir;
286         //Desired folder: /private/tmp/$UID/`uuidgen`
287         tmpDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
288         if (tmpDir) {
289                 NSString        *launchPath = [[[@"/" stringByAppendingPathComponent:@"usr"] stringByAppendingPathComponent:@"bin"] stringByAppendingPathComponent:@"zip"];
291                 //Proceed only if /usr/bin/zip exists
292                 if ([defaultManager fileExistsAtPath:launchPath]) {
293                         NSString        *folderName = [inPath lastPathComponent];
294                         NSArray         *arguments;
295                         NSTask          *zipTask = nil;
296                         
297                         BOOL            success = YES;
299                         //Ensure our temporary directory exists [it never will the first time this method is called]
300                         [defaultManager createDirectoryAtPath:tmpDir attributes:nil];
302                         pathToArchive = [[NSFileManager defaultManager] uniquePathForPath:[tmpDir stringByAppendingPathComponent:[folderName stringByAppendingPathExtension:@"zip"]]];
304                         arguments = [NSArray arrayWithObjects:
305                                 @"-r", //we'll want to store recursively
306                                 @"-1", //use the fastest level of compression that isn't storage; the user can compress manually to do better
307                                 @"-q", //shhh!
308                                 pathToArchive,   //output to our destination name
309                                 folderName, //store the folder
310                                 nil];
311                         AILog(@"-[ESFileTransferController pathToArchiveOfFolder:]: Will launch %@ with arguments %@ in directory %@",
312                                   launchPath, arguments, [inPath stringByDeletingLastPathComponent]);
313                         @try
314                         {
315                                 zipTask = [[NSTask alloc] init];
316                                 [zipTask setLaunchPath:launchPath];
317                                 [zipTask setArguments:arguments];
318                                 [zipTask setCurrentDirectoryPath:[inPath stringByDeletingLastPathComponent]];
319                                 [zipTask launch];
320                                 [zipTask waitUntilExit];
321                         } 
322                         @catch (id exc) {
323                                 success = NO;
324                         }
326                         if (success) {
327                                 success = (([zipTask terminationStatus] == -1) || ([zipTask terminationStatus] == 0));
328                         }
330                         if (!success) pathToArchive = nil;
331                         AILog(@"-[ESFileTransferController pathToArchiveOfFolder:]: Success %i (%i), so pathToArchive is %@",
332                                   success, [zipTask terminationStatus], pathToArchive);
333                         [zipTask release];
334                 }
335         }
337         return pathToArchive;
340 //Initiate sending a file at a specified path to listContact
341 - (void)sendFile:(NSString *)inPath toListContact:(AIListContact *)listContact
343         AIAccount               *account;
344         
345         if ((account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_FILE_TRANSFER_TYPE
346                                                                                                                                                   toContact:listContact]) &&
347                 [account conformsToProtocol:@protocol(AIAccount_Files)]) {
348                 NSFileManager   *defaultManager = [NSFileManager defaultManager];
349                 BOOL                    isDir;
350                 
351                 //Resolve any alias we're passed if necessary
352                 inPath = [defaultManager pathByResolvingAlias:inPath];
354                 if ([defaultManager fileExistsAtPath:inPath isDirectory:&isDir]) {
355                         //If we get a directory and the account we're sending from doesn't support folder transfers
356                         if (isDir &&
357                                 ![(AIAccount<AIAccount_Files> *)account canSendFolders]) {
358                                 inPath = [self pathToArchiveOfFolder:inPath];
359                         }
360                         
361                         if (inPath) {
362                                 long fileSize = [[[defaultManager fileAttributesAtPath:inPath
363                                                                                                                   traverseLink:YES] objectForKey:NSFileSize] longValue];
364                                 if (fileSize > 0) {
365                                         ESFileTransfer  *fileTransfer;
367                                         //Set up a fileTransfer object
368                                         fileTransfer = [self newFileTransferWithContact:listContact
369                                                                                                                  forAccount:account
370                                                                                                                            type:Outgoing_FileTransfer];
371                                         
372                                         [fileTransfer setLocalFilename:inPath];
373                                         [fileTransfer setSize:fileSize];
374                                         
375                                         //The fileTransfer object should now have everything the account needs to begin transferring
376                                         [(AIAccount<AIAccount_Files> *)account beginSendOfFileTransfer:fileTransfer];
377                                         
378                                         if (showProgressWindow) {
379                                                 [self showProgressWindowIfNotOpen:nil];
380                                         }
381                                 } else {
382                                         //XXX Show a warning message rather than just beeping
383                                         NSBeep();
384                                 }
385                         }
386                 }
387         }
390 //Menu or context menu item for sending a file was selected - possible only when a listContact is selected
391 - (IBAction)sendFileToSelectedContact:(id)sender
393         //Get the "selected" list object (contact list or message window)
394         AIListObject    *selectedObject;
395         AIListContact   *listContact = nil;
396         
397         selectedObject = [[adium interfaceController] selectedListObject];
398         if ([selectedObject isKindOfClass:[AIListContact class]]) {
399                 listContact = [[adium contactController] preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
400                                                                                                                                  forListContact:(AIListContact *)selectedObject];
401         }
402         
403         if (listContact) {
404                 [self requestForSendingFileToListContact:listContact];
405         }
407 //Prompt for a new contact with the current tab's name
408 - (IBAction)contextualMenuSendFile:(id)sender
410         AIListObject    *selectedObject = [[adium menuController] currentContextMenuObject];
411         AIListContact   *listContact = [[adium contactController] preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
412                                                                                                                                                           forListContact:(AIListContact *)selectedObject];
413         
414         [NSApp activateIgnoringOtherApps:YES];
415         [self requestForSendingFileToListContact:listContact];
418 #pragma mark Status updates
419 - (void)fileTransfer:(ESFileTransfer *)fileTransfer didSetStatus:(AIFileTransferStatus)status
421         switch (status) {
422                 case Checksumming_Filetransfer:
423                         [[adium contactAlertsController] generateEvent:FILE_TRANSFER_CHECKSUMMING
424                                                                                          forListObject:[fileTransfer contact] 
425                                                                                                   userInfo:fileTransfer
426                                                           previouslyPerformedActionIDs:nil];
427                         
428                         if (showProgressWindow) {
429                                 [self showProgressWindowIfNotOpen:nil];
430                         }       
431                         break;
432                 case Waiting_on_Remote_User_FileTransfer:
433                         [[adium contactAlertsController] generateEvent:FILE_TRANSFER_WAITING_REMOTE
434                                                                                          forListObject:[fileTransfer contact]
435                                                                                                   userInfo:fileTransfer
436                                                           previouslyPerformedActionIDs:nil];
437                         
438                         if (showProgressWindow) {
439                                 [self showProgressWindowIfNotOpen:nil];
440                         }
441                                 
442                                 break;
443                 case Accepted_FileTransfer:
444                         [[adium contactAlertsController] generateEvent:FILE_TRANSFER_BEGAN
445                                                                                          forListObject:[fileTransfer contact] 
446                                                                                                   userInfo:fileTransfer
447                                                           previouslyPerformedActionIDs:nil];
449                         if (showProgressWindow) {
450                                 [self showProgressWindowIfNotOpen:nil];
451                         }
452                         
453                         break;
454                 case Complete_FileTransfer:
455                         [[adium contactAlertsController] generateEvent:FILE_TRANSFER_COMPLETE
456                                                                                          forListObject:[fileTransfer contact] 
457                                                                                                   userInfo:fileTransfer
458                                                           previouslyPerformedActionIDs:nil];
459                         
460                         //The file is complete; if we are supposed to automatically open safe files and this is one, open it
461                         if ([self shouldOpenCompleteFileTransfer:fileTransfer]) { 
462                                 [fileTransfer openFile];
463                         }
464                         
465                         break;
466                 case Cancelled_Remote_FileTransfer:
467                         [[adium contactAlertsController] generateEvent:FILE_TRANSFER_CANCELLED
468                                                                                          forListObject:[fileTransfer contact] 
469                                                                                                   userInfo:fileTransfer
470                                                           previouslyPerformedActionIDs:nil];
471                         break;
472                 case Failed_FileTransfer:
473                         [[adium contactAlertsController] generateEvent:FILE_TRANSFER_FAILED
474                                                                                          forListObject:[fileTransfer contact] 
475                                                                                                   userInfo:fileTransfer
476                                                           previouslyPerformedActionIDs:nil];
477                         break;
478                 default:
479                         break;
480         }
483 - (BOOL)shouldOpenCompleteFileTransfer:(ESFileTransfer *)fileTransfer
485         BOOL    shouldOpen = NO;
486         
487         if (autoOpenSafe &&
488            ([fileTransfer fileTransferType] == Incoming_FileTransfer)) {
489                 
490                 if (!safeFileExtensions) safeFileExtensions = [SAFE_FILE_EXTENSIONS_SET retain];                
492                 shouldOpen = [safeFileExtensions containsObject:[[[fileTransfer localFilename] pathExtension] lowercaseString]];
493         }
495         return shouldOpen;
498 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
500         AIListContact   *listContact = nil;
501         
502     if (menuItem == menuItem_sendFile) {
503         AIListObject    *selectedObject = [[adium interfaceController] selectedListObject];
504                 if (selectedObject && [selectedObject isKindOfClass:[AIListContact class]]) {
505                         listContact = [[adium contactController] preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
506                                                                                                                                          forListContact:(AIListContact *)selectedObject];
507                 }
508                 
509                 return listContact != nil;
510                 
511         } else if (menuItem == menuItem_sendFileContext) {
512                 AIListObject    *selectedObject = [[adium menuController] currentContextMenuObject];
513                 if (selectedObject && [selectedObject isKindOfClass:[AIListContact class]]) {
514                         listContact = [[adium contactController] preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
515                                                                                                                                          forListContact:(AIListContact *)selectedObject];
516                 }
517                 
518                 return listContact != nil;
519                 
520     } else if (menuItem == menuItem_showFileTransferProgress) {
521                 return YES;
522         }
524     return YES;
528 - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
530         AIListContact   *listContact = nil;
531         
532         AIListObject    *selectedObject = [[adium interfaceController] selectedListObject];
533         if (selectedObject && [selectedObject isKindOfClass:[AIListContact class]]) {
534                 listContact = [[adium contactController] preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
535                                                                                                                                  forListContact:(AIListContact *)selectedObject];
536         }
538     return listContact != nil;
541 #pragma mark File transfer progress window
542 - (void)configureFileTransferProgressWindow
544         //Add the File Transfer Progress window menuItem
545         menuItem_showFileTransferProgress = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"File Transfers",nil)
546                                                                                                                                    target:self 
547                                                                                                                                    action:@selector(showProgressWindow:)
548                                                                                                                         keyEquivalent:@"l"];
549         [menuItem_showFileTransferProgress setKeyEquivalentModifierMask:(NSCommandKeyMask | NSAlternateKeyMask)];
550         [[adium menuController] addMenuItem:menuItem_showFileTransferProgress toLocation:LOC_Window_Auxiliary];
553 //Show the file transfer progress window
554 - (void)showProgressWindow:(id)sender
556         [ESFileTransferProgressWindowController showFileTransferProgressWindow];
559 - (void)showProgressWindowIfNotOpen:(id)sender
561         [ESFileTransferProgressWindowController showFileTransferProgressWindowIfNotOpen];       
564 #pragma mark Preferences
565 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
566                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
568         autoAcceptType = [[prefDict objectForKey:KEY_FT_AUTO_ACCEPT] intValue];
569         autoOpenSafe = [[prefDict objectForKey:KEY_FT_AUTO_OPEN_SAFE] boolValue];
570         
571         //If we created a safe file extensions set and no longer need it, desroy it
572         if (!autoOpenSafe && safeFileExtensions) {
573                 [safeFileExtensions release]; safeFileExtensions = nil;
574         }
575         
576         showProgressWindow = [[prefDict objectForKey:KEY_FT_SHOW_PROGRESS_WINDOW] boolValue];
579 #pragma mark AIEventHandler
581 - (NSString *)shortDescriptionForEventID:(NSString *)eventID
583         NSString *description;
585         if ([eventID isEqualToString:FILE_TRANSFER_FAILED]) {
586                 description = AILocalizedString(@"File transfer fails",nil);
587         } else {
588                 description = @"";
589         }
590         
591         return description;
594 - (NSString *)globalShortDescriptionForEventID:(NSString *)eventID
596         NSString        *description;
597         
598         if ([eventID isEqualToString:FILE_TRANSFER_REQUEST]) {
599                 description = AILocalizedString(@"File transfer requested",nil);
600         } else if ([eventID isEqualToString:FILE_TRANSFER_CHECKSUMMING]) {
601                 description = AILocalizedString(@"File is checksummed before sending",nil);
602         } else if ([eventID isEqualToString:FILE_TRANSFER_WAITING_REMOTE]) {
603                 description = AILocalizedString(@"File transfer being offered to other side",nil);
604         } else if ([eventID isEqualToString:FILE_TRANSFER_BEGAN]) {
605                 description = AILocalizedString(@"File transfer begins",nil);
606         } else if ([eventID isEqualToString:FILE_TRANSFER_CANCELLED]) {
607                 description = AILocalizedString(@"File transfer cancelled by the other side",nil);
608         } else if ([eventID isEqualToString:FILE_TRANSFER_COMPLETE]) {
609                 description = AILocalizedString(@"File transfer completed successfully",nil);
610         } else if ([eventID isEqualToString:FILE_TRANSFER_FAILED]) {
611                 description = AILocalizedString(@"File transfer failed",nil);
612         } else {                
613                 description = @"";      
614         }
615         
616         return description;
619 //Evan: This exists because old X(tras) relied upon matching the description of event IDs, and I don't feel like making
620 //a converter for old packs.  If anyone wants to fix this situation, please feel free :)
621 //XXX-fix this for the above comment.
622 - (NSString *)englishGlobalShortDescriptionForEventID:(NSString *)eventID
624         NSString        *description;
625         
626         if ([eventID isEqualToString:FILE_TRANSFER_REQUEST]) {
627                 description = @"File Transfer Request";
628         } else if ([eventID isEqualToString:FILE_TRANSFER_CHECKSUMMING]) {
629                 description = @"File Checksumming for Sending";
630         } else if ([eventID isEqualToString:FILE_TRANSFER_WAITING_REMOTE]) {
631                 description = @"File Transfer Being Offered to Remote User";
632         }  else if ([eventID isEqualToString:FILE_TRANSFER_BEGAN]) {
633                 description = @"File Transfer Began";
634         } else if ([eventID isEqualToString:FILE_TRANSFER_CANCELLED]) {
635                 //Canceled, not Cancelled as we use elsewhere in Adium, for historical reasons. Both are valid spellings.
636                 description = @"File Transfer Canceled Remotely";
637         } else if ([eventID isEqualToString:FILE_TRANSFER_COMPLETE]) {
638                 description = @"File Transfer Complete";
639         } else if ([eventID isEqualToString:FILE_TRANSFER_FAILED]) {
640                 description = @"File transfer failed";
641         } else {                
642                 description = @"";      
643         }
644         
645         return description;
648 - (NSString *)longDescriptionForEventID:(NSString *)eventID forListObject:(AIListObject *)listObject
649 {       
650         NSString        *description;
651         
652         if (listObject) {
653                 NSString *format;
654                 
655                 if ([eventID isEqualToString:FILE_TRANSFER_FAILED]) {
656                         format = AILocalizedString(@"When a file transfer with %@ fails",nil);  
657                 } else {
658                         format = nil;   
659                 }
660                 
661                 if (format) {
662                         NSString *name;
663                         name = ([listObject isKindOfClass:[AIListGroup class]] ?
664                                         [NSString stringWithFormat:AILocalizedString(@"a member of %@",nil),[listObject displayName]] :
665                                         [listObject displayName]);
666                         
667                         description = [NSString stringWithFormat:format, name];
669                 } else {
670                         description = @"";
671                 }
672                 
673         } else {
674                 if ([eventID isEqualToString:FILE_TRANSFER_REQUEST]) {
675                         description = AILocalizedString(@"When a file transfer is requested",nil);
676                 } else if ([eventID isEqualToString:FILE_TRANSFER_CHECKSUMMING]) {
677                         description = AILocalizedString(@"When a file is checksummed prior to sending",nil);
678                 } else if ([eventID isEqualToString:FILE_TRANSFER_WAITING_REMOTE]) {
679                         description = AILocalizedString(@"When a file transfer is offered to a remote user",nil);
680                 } else if ([eventID isEqualToString:FILE_TRANSFER_BEGAN]) {
681                         description = AILocalizedString(@"When a file transfer begins",nil);
682                 } else if ([eventID isEqualToString:FILE_TRANSFER_CANCELLED]) {
683                         description = AILocalizedString(@"When a file transfer is cancelled remotely",nil);
684                 } else if ([eventID isEqualToString:FILE_TRANSFER_COMPLETE]) {
685                         description = AILocalizedString(@"When a file transfer is completed successfully",nil);
686                 } else if ([eventID isEqualToString:FILE_TRANSFER_FAILED]) {
687                         description = AILocalizedString(@"When a file transfer fails",nil);
688                 } else {
689                         description = @"";      
690                 }
691         }
693         return description;
696 - (NSString *)naturalLanguageDescriptionForEventID:(NSString *)eventID
697                                                                                 listObject:(AIListObject *)listObject
698                                                                                   userInfo:(id)userInfo
699                                                                         includeSubject:(BOOL)includeSubject
701         NSString                *description = nil;
702         NSString                *displayName, *displayFilename;
703         ESFileTransfer  *fileTransfer;
705         NSParameterAssert([userInfo isKindOfClass:[ESFileTransfer class]]);
706         fileTransfer = (ESFileTransfer *)userInfo;
707         
708         displayName = [listObject displayName];
709         displayFilename = [fileTransfer displayFilename];
710         
711         if (includeSubject) {
712                 NSString        *format = nil;
713                 
714                 if ([eventID isEqualToString:FILE_TRANSFER_REQUEST]) {
715                         //Should only happen for an incoming transfer
716                         format = AILocalizedString(@"%@ requests to send you %@","A person is wanting to send you a file. The first %@ is a name; the second %@ is the filename of the file being sent.");
717                         
718                 } else if ([eventID isEqualToString:FILE_TRANSFER_WAITING_REMOTE]) {
719                         //Should only happen for outgoing file transfers
720                         format = AILocalizedString(@"Offering to send %@ to %@","You are offering to send a file to a remote user. The first %@ is the filename of the file being sent; the second %@ is the recipient of the file being sent.");
721                 
722                 } else if ([eventID isEqualToString:FILE_TRANSFER_BEGAN]) {
723                         if ([fileTransfer fileTransferType] == Incoming_FileTransfer) {
724                                 format = AILocalizedString(@"%@ began sending you %@","A person began sending you a file. The first %@ is a name; the second %@ is the filename of the file being sent.");
725                         } else {
726                                 format = AILocalizedString(@"%@ began receiving %@","A person began receiving a file from you. The first %@ is the recipient of the file; the second %@ is the filename of the file being sent.");
727                         }
728                 } else if ([eventID isEqualToString:FILE_TRANSFER_CANCELLED]) {
729                         format = AILocalizedString(@"%@ cancelled the transfer of %@","The other contact cancelled a file transfer in progress. The first %@ is the recipient of the file; the second %@ is the filename of the file being sent.");
730                 } else if ([eventID isEqualToString:FILE_TRANSFER_COMPLETE]) {
731                         if ([fileTransfer fileTransferType] == Incoming_FileTransfer) {
732                                 format = AILocalizedString(@"%@ sent you %@","First placeholder is a name; second is a filename");
733                         } else {
734                                 format = AILocalizedString(@"%@ received %@","First placeholder is a name; second is a filename");
735                         }
736                 } else if ([eventID isEqualToString:FILE_TRANSFER_FAILED]) {
737                         if ([fileTransfer fileTransferType] == Incoming_FileTransfer) {
738                                 format = AILocalizedString(@"%@'s transfer of %@ failed","First placeholder is a name; second is a filename");
740                         } else {
741                                 format = AILocalizedString(@"Your transfer to %@ of %@ failed","First placeholder is a name; second is a filename");                            
742                         }
743                 }
744                 
745                 if (format) {
746                         description = [NSString stringWithFormat:format,displayName,displayFilename];
747                 }
748         } else {
749                 NSString        *format = nil;
750                 
751                 if ([eventID isEqualToString:FILE_TRANSFER_REQUEST]) {
752                         //Should only happen for an incoming transfer
753                         format = AILocalizedString(@"requests to send you %@","%@ is a filename of a file being sent");
754                         
755                 }else if ([eventID isEqualToString:FILE_TRANSFER_WAITING_REMOTE]) {
756                         //Should only happen for an outgoing transfer
757                         format = AILocalizedString(@"offers to send %@","%@ is a filename of a file being sent");
758                 
759                 } else if ([eventID isEqualToString:FILE_TRANSFER_BEGAN]) {
760                         if ([fileTransfer fileTransferType] == Incoming_FileTransfer) {
761                                 format = AILocalizedString(@"began sending you %@","%@ is a filename of a file being sent");
762                         } else {
763                                 format = AILocalizedString(@"began receiving %@","%@ is a filename of a file being sent");
764                         }
765                 } else if ([eventID isEqualToString:FILE_TRANSFER_CANCELLED]) {
766                         format = AILocalizedString(@"cancelled the transfer of %@","%@ is a filename of a file being sent");
767                 } else if ([eventID isEqualToString:FILE_TRANSFER_COMPLETE]) {
768                         if ([fileTransfer fileTransferType] == Incoming_FileTransfer) {
769                                 format = AILocalizedString(@"sent you %@","%@ is a filename of a file being sent");
770                         } else {
771                                 format = AILocalizedString(@"received %@","%@ is a filename of a file being sent");
772                         }
773                 } else if ([eventID isEqualToString:FILE_TRANSFER_FAILED]) {
774                         if ([fileTransfer fileTransferType] == Incoming_FileTransfer) {
775                                 format = AILocalizedString(@"failed to send you %@","%@ is a filename of a file being sent");
776                         } else {
777                                 format = AILocalizedString(@"failed to receive %@","%@ is a filename of a file being sent");
778                         }
779                 }
781                 if (format) {
782                         description = [NSString stringWithFormat:format,displayFilename];
783                 }               
784         }
786         return description;
789 - (NSImage *)imageForEventID:(NSString *)eventID
791         static NSImage  *eventImage = nil;
792         if (!eventImage) eventImage = [[NSImage imageNamed:@"pref-ft" forClass:[self class]] retain];
793         return eventImage;
796 #pragma mark Strings for sizes
798 #define ZERO_BYTES                      AILocalizedString(@"Zero bytes", "no file size")
800 - (NSString *)stringForSize:(unsigned long long)inSize
802         NSString *ret = nil;
803         
804         if ( inSize == 0. ) ret = ZERO_BYTES;
805         else if ( inSize > 0. && inSize < 1024. ) ret = [NSString stringWithFormat:AILocalizedString( @"%llu bytes", "file size measured in bytes" ), inSize];
806         else if ( inSize >= 1024. && inSize < pow( 1024., 2. ) ) ret = [NSString stringWithFormat:AILocalizedString( @"%.1f KB", "file size measured in kilobytes" ), ( inSize / 1024. )];
807         else if ( inSize >= pow( 1024., 2. ) && inSize < pow( 1024., 3. ) ) ret = [NSString stringWithFormat:AILocalizedString( @"%.2f MB", "file size measured in megabytes" ), ( inSize / pow( 1024., 2. ) )];
808         else if ( inSize >= pow( 1024., 3. ) && inSize < pow( 1024., 4. ) ) ret = [NSString stringWithFormat:AILocalizedString( @"%.3f GB", "file size measured in gigabytes" ), ( inSize / pow( 1024., 3. ) )];
809         else if ( inSize >= pow( 1024., 4. ) ) ret = [NSString stringWithFormat:AILocalizedString( @"%.4f TB", "file size measured in terabytes" ), ( inSize / pow( 1024., 4. ) )];
810         
811         if (!ret) ret = ZERO_BYTES;
812         
813         return ret;
816 - (NSString *)stringForSize:(unsigned long long)inSize of:(unsigned long long)totalSize ofString:(NSString *)totalSizeString
818         NSString *ret = nil;
819         
820         if ( inSize == 0. ) {
821                 ret = ZERO_BYTES;
822         } else if ( inSize > 0. && inSize < 1024. ) {
823                 if ( totalSize > 0. && totalSize < 1024. ) {
824                         ret = [NSString stringWithFormat:AILocalizedString( @"%llu of %llu bytes", "file sizes both measured in bytes" ), inSize, totalSize];
825                         
826                 } else {
827                         ret = [NSString stringWithFormat:AILocalizedString( @"%llu bytes of %@", "file size measured in bytes out of some other measurement" ), inSize, totalSizeString];
828                         
829                 }
830         } else if ( inSize >= 1024. && inSize < pow( 1024., 2. ) ) {
831                 if ( totalSize >= 1024. && totalSize < pow( 1024., 2. ) ) {
832                         ret = [NSString stringWithFormat:AILocalizedString( @"%.1f of %.1f KB", "file sizes both measured in kilobytes" ), ( inSize / 1024. ), ( totalSize / 1024. )];
833                         
834                 } else {
835                         ret = [NSString stringWithFormat:AILocalizedString( @"%.1f KB of %@", "file size measured in kilobytes out of some other measurement" ), ( inSize / 1024. ), totalSizeString];
836                 }
837         }
838         else if ( inSize >= pow( 1024., 2. ) && inSize < pow( 1024., 3. ) ) {
839                 if ( totalSize >= pow( 1024., 2. ) && totalSize < pow( 1024., 3. ) ) {
840                         ret = [NSString stringWithFormat:AILocalizedString( @"%.2f of %.2f MB", "file sizes both measured in megabytes" ), ( inSize / pow( 1024., 2. ) ), ( totalSize / pow( 1024., 2. ) )];
841                 } else {
842                         ret = [NSString stringWithFormat:AILocalizedString( @"%.2f MB of %@", "file size measured in megabytes out of some other measurement" ), ( inSize / pow( 1024., 2. ) ), totalSizeString];       
843                 }
844         }
845         else if ( inSize >= pow( 1024., 3. ) && inSize < pow( 1024., 4. ) ) {
846                 if ( totalSize >= pow( 1024., 3. ) && totalSize < pow( 1024., 4. ) ) {
847                         ret = [NSString stringWithFormat:AILocalizedString( @"%.3f of %.3f GB", "file sizes both measured in gigabytes" ), ( inSize / pow( 1024., 3. ) ), ( totalSize / pow( 1024., 3. ) )];
848                 } else {
849                         ret = [NSString stringWithFormat:AILocalizedString( @"%.3f GB of %@", "file size measured in gigabytes out of some other measurement" ), ( inSize / pow( 1024., 3. ) ), totalSizeString];
850                         
851                 }
852         }
853         else if ( inSize >= pow( 1024., 4. ) ) {
854                 if ( totalSize >= pow( 1024., 4. ) ) {
855                         ret = [NSString stringWithFormat:AILocalizedString( @"%.4f of %.4f TB", "file sizes both measured in terabytes" ), ( inSize / pow( 1024., 4. ) ),  ( totalSize / pow( 1024., 4. ) )];
856                 } else {
857                         ret = [NSString stringWithFormat:AILocalizedString( @"%.4f TB of %@", "file size measured in terabytes out of some other measurement" ), ( inSize / pow( 1024., 4. ) ), totalSizeString];                       
858                 }
859         }
860         
861         if (!ret) ret = ZERO_BYTES;
862         
863         return ret;
866 @end