Put NSAutoreleasePool usage around other distributed notification observer methods
[adiumx.git] / Source / AdiumURLHandling.m
blob4b450469e24220c179bd106a678461861622e7c1
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 <Adium/AIAccountControllerProtocol.h>
18 #import <Adium/AIChatControllerProtocol.h>
19 #import <Adium/AIPreferenceControllerProtocol.h>
21 #import <Adium/AIContactControllerProtocol.h>
22 #import <Adium/AIContentControllerProtocol.h>
23 #import <Adium/AIInterfaceControllerProtocol.h>
25 #import "AdiumURLHandling.h"
26 #import "XtrasInstaller.h"
27 #import "ESTextAndButtonsWindowController.h"
28 #import "AINewContactWindowController.h"
29 #import <AIUtilities/AIStringAdditions.h>
30 #import <AIUtilities/AIURLAdditions.h>
31 #import <Adium/AIAccount.h>
32 #import <Adium/AIContentMessage.h>
33 #import <Adium/AIService.h>
35 #define GROUP_URL_HANDLING                      @"URL Handling Group"
36 #define KEY_DONT_PROMPT_FOR_URL         @"Don't Prompt for URL"
37 #define KEY_COMPLETED_FIRST_LAUNCH      @"AdiumURLHandling:CompletedFirstLaunch"
39 @interface AdiumURLHandling(PRIVATE)
40 + (void)registerAsDefaultIMClient;
41 + (void)_setHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst;
42 + (BOOL)_checkHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst;
43 + (void)_openChatToContactWithName:(NSString *)name onService:(NSString *)serviceIdentifier withMessage:(NSString *)body;
44 + (void)_openAIMGroupChat:(NSString *)roomname onExchange:(int)exchange;
45 + (void)_openXMPPGroupChat:(NSString *)name onServer:(NSString *)server withPassword:(NSString *)inPassword;
46 - (void)promptUser;
47 @end
49 @implementation AdiumURLHandling
51 + (void)registerURLTypes
53         /* TODO:
54          * Prompt the user to change Adium to be the protocol handler for aim:// and/or yahoo:// if we aren't already. Give them the option to agree, disagree, or disagree and never be asked again. 
55          */
56         ICInstance ICInst;
57         OSErr Err = noErr;
59         //Start Internet Config, passing it Adium's creator code
60         Err = ICStart(&ICInst, 'AdiM');
61         if (Err == noErr) {
62                 //Bracket multiple calls with ICBegin() for efficiency as per documentation
63                 ICBegin(ICInst, icReadWritePerm);
64                 BOOL alreadySet = YES;
66                 //Configure the protocols we want.
67                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "aim") withInstance:ICInst]; //AIM, official
68                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "ymsgr") withInstance:ICInst]; //Yahoo!, official
69                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "yahoo") withInstance:ICInst]; //Yahoo!, unofficial
70                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "xmpp") withInstance:ICInst]; //Jabber, official
71                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "jabber") withInstance:ICInst]; //Jabber, unofficial
72                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "msn") withInstance:ICInst]; //MSN, unofficial
73          
74                 if(!alreadySet)
75                 {
76                         //Ask the user
77                         AdiumURLHandling *instance = [[AdiumURLHandling alloc] init];
78                         [instance promptUser];
79                         [instance release];
80                 }
81                 //Adium xtras
82                 [self _setHelperAppForKey:(kICHelper "adiumxtra") withInstance:ICInst];
84                 //End whatever it was that ICBegin() began
85                 ICEnd(ICInst);
87                 //We're done with Internet Config, so stop it
88                 Err = ICStop(ICInst);
89                 
90                 //How there could be an error stopping Internet Config, I don't know.
91                 if (Err != noErr) {
92                         NSLog(@"Error stopping InternetConfig. Error code: %d", Err);
93                 }
94         } else {
95                 NSLog(@"Error starting InternetConfig. Error code: %d", Err);
96         }
99 + (void)registerAsDefaultIMClient
101                                 ICInstance ICInst;
102         OSErr Err = noErr;
103         
104         //Start Internet Config, passing it Adium's creator code
105         Err = ICStart(&ICInst, 'AdiM');
106         if (Err == noErr) {
107                 //Bracket multiple calls with ICBegin() for efficiency as per documentation
108                 ICBegin(ICInst, icReadWritePerm);
109                 
110                 //Configure the protocols we want.
111                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "aim") withInstance:ICInst]; //AIM, official
112                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "ymsgr") withInstance:ICInst]; //Yahoo!, official
113                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "yahoo") withInstance:ICInst]; //Yahoo!, unofficial
114                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "xmpp") withInstance:ICInst]; //Jabber, official
115                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "jabber") withInstance:ICInst]; //Jabber, unofficial
116                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "msn") withInstance:ICInst]; //MSN, unofficial
117                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "gtalk") withInstance:ICInst]; //Google Talk, official?
119                 //Adium xtras
120                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "adiumxtra") withInstance:ICInst];
121                 
122                 //End whatever it was that ICBegin() began
123                 ICEnd(ICInst);
124                 
125                 //We're done with Internet Config, so stop it
126                 Err = ICStop(ICInst);
127                 
128                 //How there could be an error stopping Internet Config, I don't know.
129                 if (Err != noErr) {
130                         NSLog(@"Error stopping InternetConfig. Error code: %d", Err);
131                 }
132         } else {
133                 NSLog(@"Error starting InternetConfig. Error code: %d", Err);
134         }
137 + (void)handleURLEvent:(NSString *)eventString
139         NSURL                           *url = [NSURL URLWithString:eventString];
140         NSObject<AIAdium>       *sharedAdium = [AIObject sharedAdiumInstance];
142         if (url) {
143                 NSString        *scheme, *newScheme;
144                 NSString        *serviceID;
146                 //make sure we have the // in ://, as it simplifies later processing.
147                 if (![[url resourceSpecifier] hasPrefix:@"//"]) {
148                         eventString = [NSString stringWithFormat:@"%@://%@", [url scheme], [url resourceSpecifier]];
149                         url = [NSURL URLWithString:eventString];
150                 }
152                 scheme = [url scheme];
154                 //map schemes to common aliases (like jabber: for xmpp:).
155                 static NSDictionary *schemeMappingDict = nil;
156                 if (!schemeMappingDict) {
157                         schemeMappingDict = [[NSDictionary alloc] initWithObjectsAndKeys:
158                                 @"ymsgr", @"yahoo",
159                                 @"xmpp", @"jabber",
160                                 nil];
161                 }
162                 newScheme = [schemeMappingDict objectForKey:scheme];
163                 if (newScheme) {
164                         scheme = newScheme;
165                         eventString = [NSString stringWithFormat:@"%@:%@", scheme, [url resourceSpecifier]];
166                         url = [NSURL URLWithString:eventString];
167                 }
169                 static NSDictionary     *schemeToServiceDict = nil;
170                 if (!schemeToServiceDict) {
171                         schemeToServiceDict = [[NSDictionary alloc] initWithObjectsAndKeys:
172                                 @"AIM",    @"aim",
173                                 @"Yahoo!", @"ymsgr",
174                                 @"Jabber", @"xmpp",
175                             @"GTalk",  @"gtalk",
176                                 @"MSN",    @"msn",
177                                 nil];
178                 }
179                 
180                 if ((serviceID = [schemeToServiceDict objectForKey:scheme])) {
181                         NSString *host = [url host];
182                         NSString *query = [url query];
183                         if ([host caseInsensitiveCompare:@"goim"] == NSOrderedSame) {
184                                 // aim://goim?screenname=tekjew
185                                 NSString        *name = [[[url queryArgumentForKey:@"screenname"] stringByDecodingURLEscapes] compactedString];
187                                 if (name) {
188                                         [self _openChatToContactWithName:name
189                                                                                    onService:serviceID 
190                                                                                  withMessage:[[url queryArgumentForKey:@"message"] stringByDecodingURLEscapes]];
191                                 }
193                         } else if ([host caseInsensitiveCompare:@"addbuddy"] == NSOrderedSame) {
194                                 // aim://addbuddy?screenname=tekjew
195                                 // aim://addbuddy?listofscreennames=screen+name1,screen+name+2&groupname=buddies
196                                 NSString        *name = [[[url queryArgumentForKey:@"screenname"] stringByDecodingURLEscapes] compactedString];
197                                 AIService       *service = [[sharedAdium accountController] firstServiceWithServiceID:serviceID];
198                                 
199                                 if (name) {
200                                         [[sharedAdium contactController] requestAddContactWithUID:name
201                                                                                                                                 service:service
202                                                                                                                                           account:nil];
203                                         
204                                 } else {
205                                         NSString                *listOfNames = [url queryArgumentForKey:@"listofscreennames"];
206                                         NSArray                 *names = [listOfNames componentsSeparatedByString:@","];
207                                         NSEnumerator    *enumerator;
208                                         
209                                         enumerator = [names objectEnumerator];
210                                         while ((name = [enumerator nextObject])) {
211                                                 NSString        *decodedName = [[name stringByDecodingURLEscapes] compactedString];
212                                                 [[sharedAdium contactController] requestAddContactWithUID:decodedName
213                                                                                                                                         service:service
214                                                                                                                                                   account:nil];
215                                         }
216                                 }
218                         } else if ([host caseInsensitiveCompare:@"sendim"] == NSOrderedSame) {
219                                 // ymsgr://sendim?tekjew
220                                 NSString *name = [[[url query] stringByDecodingURLEscapes] compactedString];
221                                 
222                                 if (name) {
223                                         [self _openChatToContactWithName:name
224                                                                                    onService:serviceID
225                                                                                  withMessage:nil];
226                                 }
227                                 
228                         } else if ([host caseInsensitiveCompare:@"im"] == NSOrderedSame) {
229                                 // ymsgr://im?to=tekjew
230                                 NSString *name = [[[url queryArgumentForKey:@"to"] stringByDecodingURLEscapes] compactedString];
231                                 
232                                 if (name) {
233                                         [self _openChatToContactWithName:name
234                                                                                    onService:serviceID
235                                                                                  withMessage:nil];
236                                 }
237                                 
238                         } else if ([host caseInsensitiveCompare:@"gochat"]  == NSOrderedSame) {
239                                 // aim://gochat?RoomName=AdiumRocks
240                                 NSString        *roomname = [[url queryArgumentForKey:@"roomname"] stringByDecodingURLEscapes];
241                                 NSString        *exchangeString = [url queryArgumentForKey:@"exchange"];
242                                 if (roomname) {
243                                         int exchange = 0;
244                                         if (exchangeString) {
245                                                 exchange = [exchangeString intValue];   
246                                         }
247                                         
248                                         [self _openAIMGroupChat:roomname onExchange:(exchange ? exchange : 4)];
249                                 }
251                         } else if ([url queryArgumentForKey:@"openChatToScreenName"]) {
252                                 // aim://openChatToScreenname?tekjew  [?]
253                                 NSString *name = [[[url queryArgumentForKey:@"openChatToScreenname"] stringByDecodingURLEscapes] compactedString];
254                                 
255                                 if (name) {
256                                         [self _openChatToContactWithName:name
257                                                                                    onService:serviceID
258                                                                                  withMessage:nil];
259                                 }
260         
261                         } else if ([url queryArgumentForKey:@"openChatToScreenName"]) {
262                                 // gtalk:chat?jid=foo@gmail.com&from_jid=bar@gmail.com
263                                 NSString *name = [[[url queryArgumentForKey:@"jid"] stringByDecodingURLEscapes] compactedString];
265                                 if (name) {
266                                         [self _openChatToContactWithName:name
267                                                                                    onService:serviceID
268                                                                                  withMessage:nil];
269                                 }
270                                 
271                         } else if ([host caseInsensitiveCompare:@"BuddyIcon"] == NSOrderedSame) {
272                                 //aim:BuddyIcon?src=http://www.nbc.com//Heroes/images/wallpapers/heroes-downloads-icon-single-48x48-07.gif
273                                 NSString *urlString = [url queryArgumentForKey:@"src"];
274                                 if ([urlString length]) {
275                                         NSURL *urlToDownload = [[NSURL alloc] initWithString:urlString];
276                                         NSData *imageData = (urlToDownload ? [NSData dataWithContentsOfURL:urlToDownload] : nil);
277                                         [urlToDownload release];
278                                         
279                                         //Should prompt for where to apply the icon?
280                                         if (imageData &&
281                                                 [[[NSImage alloc] initWithData:imageData] autorelease]) {
282                                                 //If we successfully got image data, and that data makes a valid NSImage, set it as our global buddy icon
283                                                 [[[AIObject sharedAdiumInstance] preferenceController] setPreference:imageData
284                                                                                                                                                                           forKey:KEY_USER_ICON
285                                                                                                                                                                            group:GROUP_ACCOUNT_STATUS];
286                                         }
287                                 }
288                                 
289                         //Jabber additions
290                         } else if ([query rangeOfString:@"message"].location == 0) {
291                                 //xmpp:johndoe@jabber.org?message;subject=Subject;body=Body
292                                 NSString *msg = [[url queryArgumentForKey:@"body"] stringByDecodingURLEscapes];
293                                 
294                                 [self _openChatToContactWithName:[NSString stringWithFormat:@"%@@%@", [url user], [url host]]
295                                                        onService:serviceID
296                                                      withMessage:msg];
297                         } else if ([query rangeOfString:@"roster"].location == 0
298                                    || [query rangeOfString:@"subscribe"].location == 0) {
299                                 //xmpp:johndoe@jabber.org?roster;name=John%20Doe;group=Friends
300                                 //xmpp:johndoe@jabber.org?subscribe
301                                 
302                                 //Group specification and name specification is currently ignored,
303                                 //due to limitations in the AINewContactWindowController API.
305                                 AIService *jabberService;
307                                 jabberService = [[[AIObject sharedAdiumInstance] accountController] firstServiceWithServiceID:@"Jabber"];
309                                 [AINewContactWindowController promptForNewContactOnWindow:nil
310                                                                                      name:[NSString stringWithFormat:@"%@@%@", [url user], [url host]]
311                                                                                   service:jabberService
312                                                                                                                                   account:nil];
313                         } else if ([query rangeOfString:@"remove"].location == 0
314                                    || [query rangeOfString:@"unsubscribe"].location == 0) {
315                                 // xmpp:johndoe@jabber.org?remove
316                                 // xmpp:johndoe@jabber.org?unsubscribe
317                                 
318                         } else {
319                                 if (([query caseInsensitiveCompare:@"join"] == NSOrderedSame) &&
320                                         ([scheme caseInsensitiveCompare:@"xmpp"] == NSOrderedSame)) {
321                                         NSString *password = [[url queryArgumentForKey:@"password"] stringByDecodingURLEscapes];
322                                         //TODO: password support: xmpp:darkcave@macbeth.shakespeare.lit?join;password=cauldronburn
324                                         [self _openXMPPGroupChat:[url user]
325                                                                         onServer:[url host]
326                                                                 withPassword:password];
328                                         //TODO: 
329                                 } else {
330                                         //Default to opening the host as a name.
331                                         NSString        *user = [url user];
332                                         NSString        *host = [url host];
333                                         NSString        *name;
334                                         if (user && [user length]) {
335                                                 //jabber://tekjew@jabber.org
336                                                 //msn://jdoe@hotmail.com
337                                                 name = [NSString stringWithFormat:@"%@@%@",[url user],[url host]];
338                                         } else {
339                                                 //aim://tekjew
340                                                 name = host;
341                                         }
342                                         
343                                         [self _openChatToContactWithName:[name compactedString]
344                                                                                    onService:serviceID
345                                                                                  withMessage:nil];
346                                 }
347                         }
348                         
349                 } else if ([scheme isEqualToString:@"adiumxtra"]) {
350                         //Installs an adium extra
351                         // adiumxtra://www.adiumxtras.com/path/to/xtra.zip
353                         [[XtrasInstaller installer] installXtraAtURL:url];
354                 }
355         }
358 + (void)_setHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst
360         OSErr           Err;
361         ICAppSpec       Spec;
362         ICAttr          Junk;
363         long            TheSize;
365         TheSize = sizeof(Spec);
367         // Get the current aim helper app, to fill the Spec and TheSize variables
368         Err = ICGetPref(ICInst, key, &Junk, &Spec, &TheSize);
370         //Set the name and creator codes
371         if (Spec.fCreator != 'AdIM') {
372                 Spec.name[0] = sprintf((char *) &Spec.name[1], "Adium.app");
373                 Spec.fCreator = 'AdIM';
375                 //Set the helper app to Adium
376                 Err = ICSetPref(ICInst, key, kICAttrNoChange, &Spec, TheSize);
377         }
380 + (BOOL)_checkHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst
382         OSErr           Err;
383         ICAppSpec       Spec;
384         ICAttr          Junk;
385         long            TheSize;
386         
387         TheSize = sizeof(Spec);
388         
389         // Get the current aim helper app, to fill the Spec and TheSize variables
390         Err = ICGetPref(ICInst, key, &Junk, &Spec, &TheSize);
391         
392         //Set the name and creator codes
393         return Spec.fCreator == 'AdIM';
396 + (void)_openChatToContactWithName:(NSString *)UID onService:(NSString *)serviceID withMessage:(NSString *)message
398         AIListContact           *contact;
399         NSObject<AIAdium>       *sharedAdium = [AIObject sharedAdiumInstance];
400         
401         contact = [[sharedAdium contactController] preferredContactWithUID:UID
402                                                                                                                   andServiceID:serviceID 
403                                                                                                  forSendingContentType:CONTENT_MESSAGE_TYPE];
404         if (contact) {
405                 //Open the chat and set it as active
406                 [[sharedAdium interfaceController] setActiveChat:[[sharedAdium chatController] openChatWithContact:contact
407                                                                                                                                                                                 onPreferredAccount:YES]];
408                 
409                 //Insert the message text as if the user had typed it after opening the chat
410                 NSResponder *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
411                 if (message && [responder isKindOfClass:[NSTextView class]] && [(NSTextView *)responder isEditable]) {
412                         [responder insertText:message];
413                 }
415         } else {
416                 NSBeep();
417         }
420 + (void)_openXMPPGroupChat:(NSString *)name onServer:(NSString *)server withPassword:(NSString *)password
422         AIAccount               *account;
423         NSEnumerator    *enumerator;
424         
425         //Find an XMPP-compatible online account which can create group chats
426         enumerator = [[[[AIObject sharedAdiumInstance] accountController] accounts] objectEnumerator];
427         while ((account = [enumerator nextObject])) {
428                 if ([account online] &&
429                         [[account serviceClass] isEqualToString:@"Jabber"] &&
430                         [[account service] canCreateGroupChats]) {
431                         break;
432                 }
433         }
434         
435         if (name && account) {
436                 [[[AIObject sharedAdiumInstance] chatController] chatWithName:name
437                                                                                                                    identifier:nil
438                                                                                                                         onAccount:account
439                                                                                                          chatCreationInfo:[NSDictionary dictionaryWithObjectsAndKeys:
440                                                                                                                                            name, @"room",
441                                                                                                                                            server, @"server",
442                                                                                                                                            [account formattedUID], @"handle",
443                                                                                                                                            password, @"password", /* may be nil, so should be last */
444                                                                                                                                            nil]];
445         } else {
446                 NSBeep();
447         }
450 + (void)_openAIMGroupChat:(NSString *)roomname onExchange:(int)exchange
452         AIAccount               *account;
453         NSEnumerator    *enumerator;
454         
455         //Find an AIM-compatible online account which can create group chats
456         enumerator = [[[[AIObject sharedAdiumInstance] accountController] accounts] objectEnumerator];
457         while ((account = [enumerator nextObject])) {
458                 if ([account online] &&
459                         [[account serviceClass] isEqualToString:@"AIM-compatible"] &&
460                         [[account service] canCreateGroupChats]) {
461                         break;
462                 }
463         }
464         
465         if (roomname && account) {
466                 [[[AIObject sharedAdiumInstance] chatController] chatWithName:roomname
467                                                                                                                    identifier:nil
468                                                                                                                         onAccount:account
469                                                                                                          chatCreationInfo:[NSDictionary dictionaryWithObjectsAndKeys:
470                                                                                                                                                 roomname, @"room",
471                                                                                                                                                 [NSNumber numberWithInt:exchange], @"exchange",
472                                                                                                                                                 nil]];
473         } else {
474                 NSBeep();
475         }
478 - (void)URLQuestion:(NSNumber *)number info:(id)info
480         AITextAndButtonsReturnCode ret = [number intValue];
481         switch(ret)
482         {
483                 case AITextAndButtonsOtherReturn:
484                         [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES] forKey:KEY_DONT_PROMPT_FOR_URL group:GROUP_URL_HANDLING];
485                         break;
486                 case AITextAndButtonsDefaultReturn:
487                         [AdiumURLHandling registerAsDefaultIMClient];
488                         break;
489                 case AITextAndButtonsAlternateReturn:
490                 default:
491                         break;
492         }
495 - (void)promptUser
497         if ([[[adium preferenceController] preferenceForKey:KEY_COMPLETED_FIRST_LAUNCH group:GROUP_URL_HANDLING] boolValue]) {
498                 if(![[adium preferenceController] preferenceForKey:KEY_DONT_PROMPT_FOR_URL group:GROUP_URL_HANDLING])
499                         [[adium interfaceController] displayQuestion:AILocalizedString(@"Change default messaging client?", nil)
500                                                                                  withDescription:AILocalizedString(@"Adium is not your default Instant Messaging client. The default client is loaded when you click messaging URLs in web pages. Would you like Adium to become the default?", nil)
501                                                                                  withWindowTitle:nil
502                                                                                    defaultButton:AILocalizedString(@"Yes", nil)
503                                                                                  alternateButton:AILocalizedString(@"No", nil)
504                                                                                          otherButton:AILocalizedString(@"Never", nil)
505                                                                                                   target:self
506                                                                                                 selector:@selector(URLQuestion:info:)
507                                                                                                 userInfo:nil];
508         } else {
509                 //On the first launch, simply register. If the user uses another IM client which takes control of the protocols again, we'll prompt for what to do.
510                 [AdiumURLHandling registerAsDefaultIMClient];
511                 
512                 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES]
513                                                                                          forKey:KEY_COMPLETED_FIRST_LAUNCH
514                                                                                           group:GROUP_URL_HANDLING];
515         }
518 @end