We need a b8 before we can release since we've had some significant changes since...
[adiumx.git] / Source / AdiumURLHandling.m
blobcad3036c220734a76ce77766a3f20e1561529b13
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)promptUser;
46 @end
48 @implementation AdiumURLHandling
50 + (void)registerURLTypes
52         /* TODO:
53          * 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. 
54          */
55         ICInstance ICInst;
56         OSErr Err = noErr;
58         //Start Internet Config, passing it Adium's creator code
59         Err = ICStart(&ICInst, 'AdiM');
60         if (Err == noErr) {
61                 //Bracket multiple calls with ICBegin() for efficiency as per documentation
62                 ICBegin(ICInst, icReadWritePerm);
63                 BOOL alreadySet = YES;
65                 //Configure the protocols we want.
66                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "aim") withInstance:ICInst]; //AIM, official
67                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "ymsgr") withInstance:ICInst]; //Yahoo!, official
68                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "yahoo") withInstance:ICInst]; //Yahoo!, unofficial
69                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "xmpp") withInstance:ICInst]; //Jabber, official
70                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "jabber") withInstance:ICInst]; //Jabber, unofficial
71                 alreadySet &= [self _checkHelperAppForKey:(kICHelper "msn") withInstance:ICInst]; //MSN, unofficial
72          
73                 if(!alreadySet)
74                 {
75                         //Ask the user
76                         AdiumURLHandling *instance = [[AdiumURLHandling alloc] init];
77                         [instance promptUser];
78                         [instance release];
79                 }
80                 //Adium xtras
81                 [self _setHelperAppForKey:(kICHelper "adiumxtra") withInstance:ICInst];
83                 //End whatever it was that ICBegin() began
84                 ICEnd(ICInst);
86                 //We're done with Internet Config, so stop it
87                 Err = ICStop(ICInst);
88                 
89                 //How there could be an error stopping Internet Config, I don't know.
90                 if (Err != noErr) {
91                         NSLog(@"Error stopping InternetConfig. Error code: %d", Err);
92                 }
93         } else {
94                 NSLog(@"Error starting InternetConfig. Error code: %d", Err);
95         }
98 + (void)registerAsDefaultIMClient
100                                 ICInstance ICInst;
101         OSErr Err = noErr;
102         
103         //Start Internet Config, passing it Adium's creator code
104         Err = ICStart(&ICInst, 'AdiM');
105         if (Err == noErr) {
106                 //Bracket multiple calls with ICBegin() for efficiency as per documentation
107                 ICBegin(ICInst, icReadWritePerm);
108                 
109                 //Configure the protocols we want.
110                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "aim") withInstance:ICInst]; //AIM, official
111                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "ymsgr") withInstance:ICInst]; //Yahoo!, official
112                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "yahoo") withInstance:ICInst]; //Yahoo!, unofficial
113                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "xmpp") withInstance:ICInst]; //Jabber, official
114                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "jabber") withInstance:ICInst]; //Jabber, unofficial
115                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "msn") withInstance:ICInst]; //MSN, unofficial
116                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "gtalk") withInstance:ICInst]; //Google Talk, official?
118                 //Adium xtras
119                 [AdiumURLHandling _setHelperAppForKey:(kICHelper "adiumxtra") withInstance:ICInst];
120                 
121                 //End whatever it was that ICBegin() began
122                 ICEnd(ICInst);
123                 
124                 //We're done with Internet Config, so stop it
125                 Err = ICStop(ICInst);
126                 
127                 //How there could be an error stopping Internet Config, I don't know.
128                 if (Err != noErr) {
129                         NSLog(@"Error stopping InternetConfig. Error code: %d", Err);
130                 }
131         } else {
132                 NSLog(@"Error starting InternetConfig. Error code: %d", Err);
133         }
136 + (void)handleURLEvent:(NSString *)eventString
138         NSURL                           *url = [NSURL URLWithString:eventString];
139         NSObject<AIAdium>       *sharedAdium = [AIObject sharedAdiumInstance];
141         if (url) {
142                 NSString        *scheme, *newScheme;
143                 NSString        *serviceID;
145                 //make sure we have the // in ://, as it simplifies later processing.
146                 if (![[url resourceSpecifier] hasPrefix:@"//"]) {
147                         eventString = [NSString stringWithFormat:@"%@://%@", [url scheme], [url resourceSpecifier]];
148                         url = [NSURL URLWithString:eventString];
149                 }
151                 scheme = [url scheme];
153                 //map schemes to common aliases (like jabber: for xmpp:).
154                 static NSDictionary *schemeMappingDict = nil;
155                 if (!schemeMappingDict) {
156                         schemeMappingDict = [[NSDictionary alloc] initWithObjectsAndKeys:
157                                 @"ymsgr", @"yahoo",
158                                 @"xmpp", @"jabber",
159                                 nil];
160                 }
161                 newScheme = [schemeMappingDict objectForKey:scheme];
162                 if (newScheme) {
163                         scheme = newScheme;
164                         eventString = [NSString stringWithFormat:@"%@:%@", scheme, [url resourceSpecifier]];
165                         url = [NSURL URLWithString:eventString];
166                 }
168                 static NSDictionary     *schemeToServiceDict = nil;
169                 if (!schemeToServiceDict) {
170                         schemeToServiceDict = [[NSDictionary alloc] initWithObjectsAndKeys:
171                                 @"AIM",    @"aim",
172                                 @"Yahoo!", @"ymsgr",
173                                 @"Jabber", @"xmpp",
174                             @"GTalk",  @"gtalk",
175                                 @"MSN",    @"msn",
176                                 nil];
177                 }
178                 
179                 if ((serviceID = [schemeToServiceDict objectForKey:scheme])) {
180                         NSString *host = [url host];
181                         NSString *query = [url query];
182                         if ([host caseInsensitiveCompare:@"goim"] == NSOrderedSame) {
183                                 // aim://goim?screenname=tekjew
184                                 NSString        *name = [[[url queryArgumentForKey:@"screenname"] stringByDecodingURLEscapes] compactedString];
186                                 if (name) {
187                                         [self _openChatToContactWithName:name
188                                                                                    onService:serviceID 
189                                                                                  withMessage:[[url queryArgumentForKey:@"message"] stringByDecodingURLEscapes]];
190                                 }
192                         } else if ([host caseInsensitiveCompare:@"addbuddy"] == NSOrderedSame) {
193                                 // aim://addbuddy?screenname=tekjew
194                                 // aim://addbuddy?listofscreennames=screen+name1,screen+name+2&groupname=buddies
195                                 NSString        *name = [[[url queryArgumentForKey:@"screenname"] stringByDecodingURLEscapes] compactedString];
196                                 AIService       *service = [[sharedAdium accountController] firstServiceWithServiceID:serviceID];
197                                 
198                                 if (name) {
199                                         [[sharedAdium contactController] requestAddContactWithUID:name
200                                                                                                                                 service:service
201                                                                                                                                           account:nil];
202                                         
203                                 } else {
204                                         NSString                *listOfNames = [url queryArgumentForKey:@"listofscreennames"];
205                                         NSArray                 *names = [listOfNames componentsSeparatedByString:@","];
206                                         NSEnumerator    *enumerator;
207                                         
208                                         enumerator = [names objectEnumerator];
209                                         while ((name = [enumerator nextObject])) {
210                                                 NSString        *decodedName = [[name stringByDecodingURLEscapes] compactedString];
211                                                 [[sharedAdium contactController] requestAddContactWithUID:decodedName
212                                                                                                                                         service:service
213                                                                                                                                                   account:nil];
214                                         }
215                                 }
217                         } else if ([host caseInsensitiveCompare:@"sendim"] == NSOrderedSame) {
218                                 // ymsgr://sendim?tekjew
219                                 NSString *name = [[[url query] stringByDecodingURLEscapes] compactedString];
220                                 
221                                 if (name) {
222                                         [self _openChatToContactWithName:name
223                                                                                    onService:serviceID
224                                                                                  withMessage:nil];
225                                 }
226                                 
227                         } else if ([host caseInsensitiveCompare:@"im"] == NSOrderedSame) {
228                                 // ymsgr://im?to=tekjew
229                                 NSString *name = [[[url queryArgumentForKey:@"to"] stringByDecodingURLEscapes] compactedString];
230                                 
231                                 if (name) {
232                                         [self _openChatToContactWithName:name
233                                                                                    onService:serviceID
234                                                                                  withMessage:nil];
235                                 }
236                                 
237                         } else if ([host caseInsensitiveCompare:@"gochat"]  == NSOrderedSame) {
238                                 // aim://gochat?RoomName=AdiumRocks
239                                 NSString        *roomname = [[url queryArgumentForKey:@"roomname"] stringByDecodingURLEscapes];
240                                 NSString        *exchangeString = [url queryArgumentForKey:@"exchange"];
241                                 if (roomname) {
242                                         int exchange = 0;
243                                         if (exchangeString) {
244                                                 exchange = [exchangeString intValue];   
245                                         }
246                                         
247                                         [self _openAIMGroupChat:roomname onExchange:(exchange ? exchange : 4)];
248                                 }
250                         } else if ([url queryArgumentForKey:@"openChatToScreenName"]) {
251                                 // aim://openChatToScreenname?tekjew  [?]
252                                 NSString *name = [[[url queryArgumentForKey:@"openChatToScreenname"] stringByDecodingURLEscapes] compactedString];
253                                 
254                                 if (name) {
255                                         [self _openChatToContactWithName:name
256                                                                                    onService:serviceID
257                                                                                  withMessage:nil];
258                                 }
259         
260                         } else if ([url queryArgumentForKey:@"openChatToScreenName"]) {
261                                 // gtalk:chat?jid=foo@gmail.com&from_jid=bar@gmail.com
262                                 NSString *name = [[[url queryArgumentForKey:@"jid"] stringByDecodingURLEscapes] compactedString];
264                                 if (name) {
265                                         [self _openChatToContactWithName:name
266                                                                                    onService:serviceID
267                                                                                  withMessage:nil];
268                                 }
269                                 
270                         } else if ([host caseInsensitiveCompare:@"BuddyIcon"] == NSOrderedSame) {
271                                 //aim:BuddyIcon?src=http://www.nbc.com//Heroes/images/wallpapers/heroes-downloads-icon-single-48x48-07.gif
272                                 NSString *urlString = [url queryArgumentForKey:@"src"];
273                                 if ([urlString length]) {
274                                         NSURL *urlToDownload = [[NSURL alloc] initWithString:urlString];
275                                         NSData *imageData = (urlToDownload ? [NSData dataWithContentsOfURL:urlToDownload] : nil);
276                                         [urlToDownload release];
277                                         
278                                         //Should prompt for where to apply the icon?
279                                         if (imageData &&
280                                                 [[[NSImage alloc] initWithData:imageData] autorelease]) {
281                                                 //If we successfully got image data, and that data makes a valid NSImage, set it as our global buddy icon
282                                                 [[[AIObject sharedAdiumInstance] preferenceController] setPreference:imageData
283                                                                                                                                                                           forKey:KEY_USER_ICON
284                                                                                                                                                                            group:GROUP_ACCOUNT_STATUS];
285                                         }
286                                 }
287                                 
288                         //Jabber additions
289                         } else if ([query rangeOfString:@"message"].location == 0) {
290                                 //xmpp:johndoe@jabber.org?message;subject=Subject;body=Body
291                                 NSString *msg = [[url queryArgumentForKey:@"body"] stringByDecodingURLEscapes];
292                                 
293                                 [self _openChatToContactWithName:[NSString stringWithFormat:@"%@@%@", [url user], [url host]]
294                                                        onService:serviceID
295                                                      withMessage:msg];
296                         } else if ([query rangeOfString:@"roster"].location == 0
297                                    || [query rangeOfString:@"subscribe"].location == 0) {
298                                 //xmpp:johndoe@jabber.org?roster;name=John%20Doe;group=Friends
299                                 //xmpp:johndoe@jabber.org?subscribe
300                                 
301                                 //Group specification and name specification is currently ignored,
302                                 //due to limitations in the AINewContactWindowController API.
304                                 AIService *jabberService;
306                                 jabberService = [[[AIObject sharedAdiumInstance] accountController] firstServiceWithServiceID:@"Jabber"];
308                                 [AINewContactWindowController promptForNewContactOnWindow:nil
309                                                                                      name:[NSString stringWithFormat:@"%@@%@", [url user], [url host]]
310                                                                                   service:jabberService
311                                                                                                                                   account:nil];
312                         } else if ([query rangeOfString:@"remove"].location == 0
313                                    || [query rangeOfString:@"unsubscribe"].location == 0) {
314                                 // xmpp:johndoe@jabber.org?remove
315                                 // xmpp:johndoe@jabber.org?unsubscribe
316                                 
317                         } else {
318                                 //Default to opening the host as a name.
319                                 NSString        *user = [url user];
320                                 NSString        *host = [url host];
321                                 NSString        *name;
322                                 if (user && [user length]) {
323                                         //jabber://tekjew@jabber.org
324                                         //msn://jdoe@hotmail.com
325                                         name = [NSString stringWithFormat:@"%@@%@",[url user],[url host]];
326                                 } else {
327                                         //aim://tekjew
328                                         name = host;
329                                 }
331                                 [self _openChatToContactWithName:[name compactedString]
332                                                                            onService:serviceID
333                                                                          withMessage:nil];
334                         }
335                         
336                 } else if ([scheme isEqualToString:@"adiumxtra"]) {
337                         //Installs an adium extra
338                         // adiumxtra://www.adiumxtras.com/path/to/xtra.zip
340                         [[XtrasInstaller installer] installXtraAtURL:url];
341                 }
342         }
345 + (void)_setHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst
347         OSErr           Err;
348         ICAppSpec       Spec;
349         ICAttr          Junk;
350         long            TheSize;
352         TheSize = sizeof(Spec);
354         // Get the current aim helper app, to fill the Spec and TheSize variables
355         Err = ICGetPref(ICInst, key, &Junk, &Spec, &TheSize);
357         //Set the name and creator codes
358         if (Spec.fCreator != 'AdIM') {
359                 Spec.name[0] = sprintf((char *) &Spec.name[1], "Adium.app");
360                 Spec.fCreator = 'AdIM';
362                 //Set the helper app to Adium
363                 Err = ICSetPref(ICInst, key, kICAttrNoChange, &Spec, TheSize);
364         }
367 + (BOOL)_checkHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst
369         OSErr           Err;
370         ICAppSpec       Spec;
371         ICAttr          Junk;
372         long            TheSize;
373         
374         TheSize = sizeof(Spec);
375         
376         // Get the current aim helper app, to fill the Spec and TheSize variables
377         Err = ICGetPref(ICInst, key, &Junk, &Spec, &TheSize);
378         
379         //Set the name and creator codes
380         return Spec.fCreator == 'AdIM';
383 + (void)_openChatToContactWithName:(NSString *)UID onService:(NSString *)serviceID withMessage:(NSString *)message
385         AIListContact           *contact;
386         NSObject<AIAdium>       *sharedAdium = [AIObject sharedAdiumInstance];
387         
388         contact = [[sharedAdium contactController] preferredContactWithUID:UID
389                                                                                                                   andServiceID:serviceID 
390                                                                                                  forSendingContentType:CONTENT_MESSAGE_TYPE];
391         if (contact) {
392                 //Open the chat and set it as active
393                 [[sharedAdium interfaceController] setActiveChat:[[sharedAdium chatController] openChatWithContact:contact
394                                                                                                                                                                                 onPreferredAccount:YES]];
395                 
396                 //Insert the message text as if the user had typed it after opening the chat
397                 NSResponder *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
398                 if (message && [responder isKindOfClass:[NSTextView class]] && [(NSTextView *)responder isEditable]) {
399                         [responder insertText:message];
400                 }
402         } else {
403                 NSBeep();
404         }
407 + (void)_openAIMGroupChat:(NSString *)roomname onExchange:(int)exchange
409         AIAccount               *account;
410         NSEnumerator    *enumerator;
411         
412         //Find an AIM-compatible online account which can create group chats
413         enumerator = [[[[AIObject sharedAdiumInstance] accountController] accounts] objectEnumerator];
414         while ((account = [enumerator nextObject])) {
415                 if ([account online] &&
416                         [[account serviceClass] isEqualToString:@"AIM-compatible"] &&
417                         [[account service] canCreateGroupChats]) {
418                         break;
419                 }
420         }
421         
422         if (roomname && account) {
423                 [[[AIObject sharedAdiumInstance] chatController] chatWithName:roomname
424                                                                                                                    identifier:nil
425                                                                                                                         onAccount:account
426                                                                                                          chatCreationInfo:[NSDictionary dictionaryWithObjectsAndKeys:
427                                                                                                                                                 roomname, @"room",
428                                                                                                                                                 [NSNumber numberWithInt:exchange], @"exchange",
429                                                                                                                                                 nil]];
430         } else {
431                 NSBeep();
432         }
435 - (void)URLQuestion:(NSNumber *)number info:(id)info
437         AITextAndButtonsReturnCode ret = [number intValue];
438         switch(ret)
439         {
440                 case AITextAndButtonsOtherReturn:
441                         [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES] forKey:KEY_DONT_PROMPT_FOR_URL group:GROUP_URL_HANDLING];
442                         break;
443                 case AITextAndButtonsDefaultReturn:
444                         [AdiumURLHandling registerAsDefaultIMClient];
445                         break;
446                 case AITextAndButtonsAlternateReturn:
447                 default:
448                         break;
449         }
452 - (void)promptUser
454         if ([[[adium preferenceController] preferenceForKey:KEY_COMPLETED_FIRST_LAUNCH group:GROUP_URL_HANDLING] boolValue]) {
455                 if(![[adium preferenceController] preferenceForKey:KEY_DONT_PROMPT_FOR_URL group:GROUP_URL_HANDLING])
456                         [[adium interfaceController] displayQuestion:AILocalizedString(@"Change default messaging client?", nil)
457                                                                                  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)
458                                                                                  withWindowTitle:nil
459                                                                                    defaultButton:AILocalizedString(@"Yes", nil)
460                                                                                  alternateButton:AILocalizedString(@"No", nil)
461                                                                                          otherButton:AILocalizedString(@"Never", nil)
462                                                                                                   target:self
463                                                                                                 selector:@selector(URLQuestion:info:)
464                                                                                                 userInfo:nil];
465         } else {
466                 //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.
467                 [AdiumURLHandling registerAsDefaultIMClient];
468                 
469                 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES]
470                                                                                          forKey:KEY_COMPLETED_FIRST_LAUNCH
471                                                                                           group:GROUP_URL_HANDLING];
472         }
475 @end