2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import <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;
49 @implementation AdiumURLHandling
51 + (void)registerURLTypes
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.
59 //Start Internet Config, passing it Adium's creator code
60 Err = ICStart(&ICInst, 'AdiM');
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
77 AdiumURLHandling *instance = [[AdiumURLHandling alloc] init];
78 [instance promptUser];
82 [self _setHelperAppForKey:(kICHelper "adiumxtra") withInstance:ICInst];
84 //End whatever it was that ICBegin() began
87 //We're done with Internet Config, so stop it
90 //How there could be an error stopping Internet Config, I don't know.
92 NSLog(@"Error stopping InternetConfig. Error code: %d", Err);
95 NSLog(@"Error starting InternetConfig. Error code: %d", Err);
99 + (void)registerAsDefaultIMClient
104 //Start Internet Config, passing it Adium's creator code
105 Err = ICStart(&ICInst, 'AdiM');
107 //Bracket multiple calls with ICBegin() for efficiency as per documentation
108 ICBegin(ICInst, icReadWritePerm);
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?
120 [AdiumURLHandling _setHelperAppForKey:(kICHelper "adiumxtra") withInstance:ICInst];
122 //End whatever it was that ICBegin() began
125 //We're done with Internet Config, so stop it
126 Err = ICStop(ICInst);
128 //How there could be an error stopping Internet Config, I don't know.
130 NSLog(@"Error stopping InternetConfig. Error code: %d", Err);
133 NSLog(@"Error starting InternetConfig. Error code: %d", Err);
137 + (void)handleURLEvent:(NSString *)eventString
139 NSURL *url = [NSURL URLWithString:eventString];
140 NSObject<AIAdium> *sharedAdium = [AIObject sharedAdiumInstance];
143 NSString *scheme, *newScheme;
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];
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:
162 newScheme = [schemeMappingDict objectForKey:scheme];
165 eventString = [NSString stringWithFormat:@"%@:%@", scheme, [url resourceSpecifier]];
166 url = [NSURL URLWithString:eventString];
169 static NSDictionary *schemeToServiceDict = nil;
170 if (!schemeToServiceDict) {
171 schemeToServiceDict = [[NSDictionary alloc] initWithObjectsAndKeys:
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];
188 [self _openChatToContactWithName:name
190 withMessage:[[url queryArgumentForKey:@"message"] stringByDecodingURLEscapes]];
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];
200 [[sharedAdium contactController] requestAddContactWithUID:name
205 NSString *listOfNames = [url queryArgumentForKey:@"listofscreennames"];
206 NSArray *names = [listOfNames componentsSeparatedByString:@","];
207 NSEnumerator *enumerator;
209 enumerator = [names objectEnumerator];
210 while ((name = [enumerator nextObject])) {
211 NSString *decodedName = [[name stringByDecodingURLEscapes] compactedString];
212 [[sharedAdium contactController] requestAddContactWithUID:decodedName
218 } else if ([host caseInsensitiveCompare:@"sendim"] == NSOrderedSame) {
219 // ymsgr://sendim?tekjew
220 NSString *name = [[[url query] stringByDecodingURLEscapes] compactedString];
223 [self _openChatToContactWithName:name
228 } else if ([host caseInsensitiveCompare:@"im"] == NSOrderedSame) {
229 // ymsgr://im?to=tekjew
230 NSString *name = [[[url queryArgumentForKey:@"to"] stringByDecodingURLEscapes] compactedString];
233 [self _openChatToContactWithName:name
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"];
244 if (exchangeString) {
245 exchange = [exchangeString intValue];
248 [self _openAIMGroupChat:roomname onExchange:(exchange ? exchange : 4)];
251 } else if ([url queryArgumentForKey:@"openChatToScreenName"]) {
252 // aim://openChatToScreenname?tekjew [?]
253 NSString *name = [[[url queryArgumentForKey:@"openChatToScreenname"] stringByDecodingURLEscapes] compactedString];
256 [self _openChatToContactWithName:name
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];
266 [self _openChatToContactWithName:name
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];
279 //Should prompt for where to apply the icon?
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
285 group:GROUP_ACCOUNT_STATUS];
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];
294 [self _openChatToContactWithName:[NSString stringWithFormat:@"%@@%@", [url user], [url host]]
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
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
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
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]
326 withPassword:password];
330 //Default to opening the host as a name.
331 NSString *user = [url user];
332 NSString *host = [url host];
334 if (user && [user length]) {
335 //jabber://tekjew@jabber.org
336 //msn://jdoe@hotmail.com
337 name = [NSString stringWithFormat:@"%@@%@",[url user],[url host]];
343 [self _openChatToContactWithName:[name compactedString]
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];
358 + (void)_setHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst
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);
380 + (BOOL)_checkHelperAppForKey:(ConstStr255Param)key withInstance:(ICInstance)ICInst
387 TheSize = sizeof(Spec);
389 // Get the current aim helper app, to fill the Spec and TheSize variables
390 Err = ICGetPref(ICInst, key, &Junk, &Spec, &TheSize);
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];
401 contact = [[sharedAdium contactController] preferredContactWithUID:UID
402 andServiceID:serviceID
403 forSendingContentType:CONTENT_MESSAGE_TYPE];
405 //Open the chat and set it as active
406 [[sharedAdium interfaceController] setActiveChat:[[sharedAdium chatController] openChatWithContact:contact
407 onPreferredAccount:YES]];
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];
420 + (void)_openXMPPGroupChat:(NSString *)name onServer:(NSString *)server withPassword:(NSString *)password
423 NSEnumerator *enumerator;
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]) {
435 if (name && account) {
436 [[[AIObject sharedAdiumInstance] chatController] chatWithName:name
439 chatCreationInfo:[NSDictionary dictionaryWithObjectsAndKeys:
442 [account formattedUID], @"handle",
443 password, @"password", /* may be nil, so should be last */
450 + (void)_openAIMGroupChat:(NSString *)roomname onExchange:(int)exchange
453 NSEnumerator *enumerator;
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]) {
465 if (roomname && account) {
466 [[[AIObject sharedAdiumInstance] chatController] chatWithName:roomname
469 chatCreationInfo:[NSDictionary dictionaryWithObjectsAndKeys:
471 [NSNumber numberWithInt:exchange], @"exchange",
478 - (void)URLQuestion:(NSNumber *)number info:(id)info
480 AITextAndButtonsReturnCode ret = [number intValue];
483 case AITextAndButtonsOtherReturn:
484 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES] forKey:KEY_DONT_PROMPT_FOR_URL group:GROUP_URL_HANDLING];
486 case AITextAndButtonsDefaultReturn:
487 [AdiumURLHandling registerAsDefaultIMClient];
489 case AITextAndButtonsAlternateReturn:
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)
502 defaultButton:AILocalizedString(@"Yes", nil)
503 alternateButton:AILocalizedString(@"No", nil)
504 otherButton:AILocalizedString(@"Never", nil)
506 selector:@selector(URLQuestion:info:)
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];
512 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES]
513 forKey:KEY_COMPLETED_FIRST_LAUNCH
514 group:GROUP_URL_HANDLING];