// // GTMGetURLHandler.m // // Copyright 2008 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. // // Add this class to your app to have get URL handled almost automatically for // you. For each entry in your CFBundleURLTypes dictionaries, add a new // key/object pair of GTMBundleURLClass/the name of the class you want // to have handle the scheme(s). // Then have that class respond to the class method: // + (BOOL)gtm_openURL:(NSURL*)url // and voila, it will just work. // Note that in Debug mode we will do extensive testing to make sure that this // is all hooked up correctly, and will spew out to the console if we // find anything amiss. // // Example plist entry // ... // // CFBundleURLTypes // // // CFBundleURLName // Google Suggestion URL // GTMBundleURLClass // GoogleSuggestURLHandler // CFBundleURLSchemes // // googlesuggest // googlesuggestextreme // // // // // // Example implementation // @interface GoogleSuggestURLHandler // @end // @implementation GoogleSuggestURLHandler // + (BOOL)gtm_openURL:(NSURL*)url { // NSLog(@"%@", url); // } // @end #import #import "GTMGarbageCollection.h" #import "GTMNSAppleEventDescriptor+Foundation.h" #import "GTMMethodCheck.h" static NSString *const kGTMBundleURLClassKey = @"GTMBundleURLClass"; // A variety of constants Apple really should have defined somewhere to // allow the compiler to find your typos. static NSString *const kGTMCFBundleURLSchemesKey = @"CFBundleURLSchemes"; static NSString *const kGTMCFBundleURLNameKey = @"CFBundleURLName"; static NSString *const kGTMCFBundleTypeRoleKey = @"CFBundleTypeRole"; static NSString *const kGTMCFBundleURLTypesKey = @"CFBundleURLTypes"; static NSString *const kGTMCFBundleViewerRole = @"Viewer"; static NSString *const kGTMCFBundleEditorRole = @"Editor"; // Set this macro elsewhere is you want to force the // bundle checks on/off. They are nice for debugging // problems, but shouldn't be required in a release version // unless you are paranoid about your users messing with your // Info.plist #ifndef GTM_CHECK_BUNDLE_URL_CLASSES #define GTM_CHECK_BUNDLE_URL_CLASSES DEBUG #endif // GTM_CHECK_BUNDLE_URL_CLASSES @protocol GTMGetURLHandlerProtocol + (BOOL)gtm_openURL:(NSURL*)url; @end @interface GTMGetURLHandler : NSObject { NSArray *urlTypes_; } - (id)initWithTypes:(NSArray*)urlTypes; - (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent; - (void)addError:(OSStatus)error withDescription:(NSString*)string toDescriptor:(NSAppleEventDescriptor *)desc; + (id)handlerForBundle:(NSBundle *)bundle; + (void)appFinishedLaunchingHandler:(NSNotification*)notification; @end @implementation GTMGetURLHandler GTM_METHOD_CHECK(NSNumber, gtm_appleEventDescriptor); GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor); + (void)load { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(appFinishedLaunchingHandler:) name:NSApplicationDidFinishLaunchingNotification object:nil]; [pool release]; } + (void)appFinishedLaunchingHandler:(NSNotification*)notification { NSBundle *bundle = [NSBundle mainBundle]; GTMGetURLHandler *handler = [GTMGetURLHandler handlerForBundle:bundle]; if (handler) { [handler retain]; GTMNSMakeUncollectable(handler); NSAppleEventManager *man = [NSAppleEventManager sharedAppleEventManager]; [man setEventHandler:handler andSelector:@selector(getUrl:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; } NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc removeObserver:self name:NSApplicationDidFinishLaunchingNotification object:nil]; } + (id)handlerForBundle:(NSBundle *)bundle { GTMGetURLHandler *handler = nil; NSArray *urlTypes = [bundle objectForInfoDictionaryKey:kGTMCFBundleURLTypesKey]; if (urlTypes) { handler = [[[GTMGetURLHandler alloc] initWithTypes:urlTypes] autorelease]; } else { _GTMDevLog(@"If you don't have CFBundleURLTypes in your plist, you may want" @" to remove GTMGetURLHandler.m from your project"); } return handler; } - (id)initWithTypes:(NSArray*)urlTypes { if ((self = [super init])) { urlTypes_ = [urlTypes retain]; #if GTM_CHECK_BUNDLE_URL_CLASSES // Some debug handling to check to make sure we can handle the // classes properly. NSEnumerator *enumerator = [urlTypes_ objectEnumerator]; NSDictionary *urlType; while ((urlType = [enumerator nextObject])) { NSString *className = [urlType objectForKey:kGTMBundleURLClassKey]; if ([className length]) { Class cls = NSClassFromString(className); if (cls) { if (![cls respondsToSelector:@selector(gtm_openURL:)]) { _GTMDevLog(@"Class %@ for URL handler %@ " "(URL schemes: %@) doesn't respond to openURL:", className, [urlType objectForKey:kGTMCFBundleURLNameKey], [urlType objectForKey:kGTMCFBundleURLSchemesKey]); } } else { _GTMDevLog(@"Unable to get class %@ for URL handler %@ " "(URL schemes: %@)", className, [urlType objectForKey:kGTMCFBundleURLNameKey], [urlType objectForKey:kGTMCFBundleURLSchemesKey]); } } else { NSString *role = [urlType objectForKey:kGTMCFBundleTypeRoleKey]; if ([role caseInsensitiveCompare:kGTMCFBundleViewerRole] == NSOrderedSame || [role caseInsensitiveCompare:kGTMCFBundleEditorRole] == NSOrderedSame) { _GTMDevLog(@"Missing %@ for URL handler %@ " "(URL schemes: %@)", kGTMBundleURLClassKey, [urlType objectForKey:kGTMCFBundleURLNameKey], [urlType objectForKey:kGTMCFBundleURLSchemesKey]); } } } #endif // GTM_CHECK_BUNDLE_URL_CLASSES } return self; } - (void)dealloc { [urlTypes_ release]; [super dealloc]; } - (NSURL*)extractURLFromEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { NSAppleEventDescriptor *desc = [event paramDescriptorForKeyword:keyDirectObject]; NSString *urlstring = [desc stringValue]; NSURL *url = [NSURL URLWithString:urlstring]; if (!url) { [self addError:errAECoercionFail withDescription:@"Unable to extract url from key direct object." toDescriptor:replyEvent]; } return url; } - (Class)getClassForScheme:(NSString *)scheme withReplyEvent:(NSAppleEventDescriptor*)replyEvent { NSEnumerator *typeEnumerator = [urlTypes_ objectEnumerator]; NSDictionary *urlType; Class cls = nil; NSString *typeScheme = nil; while (!typeScheme && (urlType = [typeEnumerator nextObject])) { NSArray *schemes = [urlType objectForKey:kGTMCFBundleURLSchemesKey]; NSEnumerator *schemeEnumerator = [schemes objectEnumerator]; while ((typeScheme = [schemeEnumerator nextObject])) { if ([typeScheme caseInsensitiveCompare:scheme] == NSOrderedSame) { break; } } } if (typeScheme) { NSString *class = [urlType objectForKey:kGTMBundleURLClassKey]; if (class) { cls = NSClassFromString(class); } if (!cls) { NSString *errorString = [NSString stringWithFormat:@"Unable to instantiate class for " "%@:%@ for scheme:%@.", kGTMBundleURLClassKey, class, typeScheme]; [self addError:errAECorruptData withDescription:errorString toDescriptor:replyEvent]; } } else { NSString *errorString = [NSString stringWithFormat:@"Unable to find handler for scheme %@.", scheme]; [self addError:errAECorruptData withDescription:errorString toDescriptor:replyEvent]; } return cls; } - (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { NSURL *url = [self extractURLFromEvent:event withReplyEvent:replyEvent]; if (!url) { return; } NSString *scheme = [url scheme]; Class cls = [self getClassForScheme:scheme withReplyEvent:replyEvent]; if (!cls) { return; } BOOL wasGood = [cls gtm_openURL:url]; if (!wasGood) { NSString *errorString = [NSString stringWithFormat:@"[%@ gtm_openURL:] failed to handle %@", NSStringFromClass(cls), url]; [self addError:errAEEventNotHandled withDescription:errorString toDescriptor:replyEvent]; } } - (void)addError:(OSStatus)error withDescription:(NSString*)string toDescriptor:(NSAppleEventDescriptor *)desc { NSAppleEventDescriptor *errorDesc = nil; if (error != noErr) { NSNumber *errNum = [NSNumber numberWithLong:error]; errorDesc = [errNum gtm_appleEventDescriptor]; [desc setParamDescriptor:errorDesc forKeyword:keyErrorNumber]; } if (string) { errorDesc = [string gtm_appleEventDescriptor]; [desc setParamDescriptor:errorDesc forKeyword:keyErrorString]; } } @end